代码随想录拓展day6 N皇后
只有这一个内容。一刷的时候也没弄太明白,二刷的时候补上。还有部分内容来自牛客网左老师的算法课程。
总体思路不容易想明白,优化也有很大难度。这要是面试能碰上基本就是故意不给过了吧。
思路
首先来看一下皇后们的约束条件:
- 不能同行
- 不能同列
- 不能同斜线
确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树。
下面我用一个 3 * 3 的棋盘,将搜索过程抽象为一棵树,如图:
从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。
那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。
回溯三部曲
按照我总结的如下回溯模板,我们来依次分析:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
- 递归函数参数
我依然是定义全局变量二维数组result来记录最终结果。
参数n是棋盘的大小,然后用row来记录当前遍历到棋盘的第几层了。
代码如下:
vector<vector<string>> result;
void backtracking(int n, int row, vector<string>& chessboard) {
- 递归终止条件
在如下树形结构中:
可以看出,当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。
代码如下:
if (row == n) {
result.push_back(chessboard);
return;
}
- 单层搜索的逻辑
递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。
每次都是要从新的一行的起始位置开始搜,所以都是从0开始。
代码如下:
for (int col = 0; col < n; col++) {
if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
chessboard[row][col] = 'Q'; // 放置皇后
backtracking(n, row + 1, chessboard);
chessboard[row][col] = '.'; // 回溯,撤销皇后
}
}
- 验证棋盘是否合法
按照如下标准去重:
- 不能同行
- 不能同列
- 不能同斜线 (45度和135度角)
代码如下:
bool isValid(int row, int col, vector<string>& chessboard, int n) {
// 检查列
for (int i = 0; i < row; i++) { // 这是一个剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查 45度角是否有皇后
for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查 135度角是否有皇后
for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
在这份代码中,细心的同学可以发现为什么没有在同行进行检查呢?
因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。
那么按照这个模板不难写出如下C++代码:
class Solution {
private:
vector<vector<string>> result;
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行了
void backtracking(int n, int row, vector<string>& chessboard) {
if (row == n) {
result.push_back(chessboard);
return;
}
for (int col = 0; col < n; col++) {
if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
chessboard[row][col] = 'Q'; // 放置皇后
backtracking(n, row + 1, chessboard);
chessboard[row][col] = '.'; // 回溯,撤销皇后
}
}
}
bool isValid(int row, int col, vector<string>& chessboard, int n) {
// 检查列
for (int i = 0; i < row; i++) { // 这是一个剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查 45度角是否有皇后
for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查 135度角是否有皇后
for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
public:
vector<vector<string>> solveNQueens(int n) {
result.clear();
std::vector<std::string> chessboard(n, std::string(n, '.'));
backtracking(n, 0, chessboard);
return result;
}
};
可以看出,除了验证棋盘合法性的代码,剩下的部分就是按照回溯法模板来的。
左老师的方法
class Solution {
public:
// 潜台词:record[0..i-1]的皇后,任何两个皇后一定都不共行、不共列,不共斜线
// 目前来到了第i行
// record[0..i-1]表示之前的行,放了的皇后位置
// n代表整体一共有多少行
// 返回值是,摆完所有的皇后,合理的摆法有多少种
int process1(int i, vector<int> record, int n){
if(i == n){
return 1;
}
int res = 0;
for(int j = 0; j < n; j++){
if(isValid(record, i, j)){// 当前行在i行,尝试i行所有的列 -> j
// 当前i行的皇后,放在j列,会不会和之前(0..i-1)的皇后,不共行共列或者共斜线,
// 如果是,认为有效
// 如果不是,认为无效
record[i] = j;
res += process1(i + 1, record, n);
}
}
return res;
}
// record[0..i-1]你需要看,record[i...]不需要看
// 返回i行皇后,放在了j列,是否有效
bool isValid(vector<int> record, int i, int j){
for(int k = 0; k < i; k++){ // 之前的某个k行的皇后
if(record[k] == j || abs(record[k] - j) == abs(i - k)){
return false;
}
}
return true;
}
int totalNQueens(int n) {
// 用一个一维数组来模拟了二维数组
vector<int> record(n, 0); // record[i] -> i行的皇后,放在了第几列
return process1(0, record, n);
}
};
看了一下好像有回溯,又好像不是特别明显。
比如说回溯遍历的过程:
int process1(int i, vector<int> record, int n){
if(i == n){
return 1;
}
int res = 0;
for(int j = 0; j < n; j++){
if(isValid(record, i, j)){
record[i] = j; // 相当于卡哥方法中的chessboard的变化
res += process1(i + 1, record, n);
// record[i] = 0; // 这一步相当于一个回溯的过程,但是在此可以不加
}
}
return res;
}
为什么卡哥的回溯过程就是:
for (int col = 0; col < n; col++) {
if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
chessboard[row][col] = 'Q'; // 放置皇后
backtracking(n, row + 1, chessboard);
chessboard[row][col] = '.'; // 回溯,撤销皇后
}
}
而左老师的过程只有:
for(int j = 0; j < n; j++){
if(isValid(record, i, j)){
record[i] = j;
res += process1(i + 1, record, n);
}
}
因为是用一个一维数组代替了二维数组。i 是控制行的,i+1也就是到了下一行,那么就是说 j 在遍历到 n 的过程就是在遍历每一列。这样如果当前位置符合条件了,记录下的 j 就是当前 i 行这一层的这一列可以放一个Q。在j++遍历的过程中,也就是尝试 j 在当前行其他列的可能。因为有效的判定是判断这一行之前的所有行列是否符合条件,因此 j 表示在同一行自然也不会被判断进去,也就不用回溯,或者说j++的过程就是在回溯。
位运算优化加速
随缘记录,不一定理解,不一定会。
class Solution {
public:
int num2(int n){
int limit = n == 32 ? -1 : (1 << n) - 1;
return process2(limit, 0, 0, 0);
}
// colLim 列的限制,1的位置不能放皇后,0的位置可以
// leftDiaLim 左斜线的限制,1的位置不能放皇后,0的位置可以
// rightDiaLim 右斜线的限制,1的位置不能放皇后,0的位置可以
int process2(int limit, int colLim, int leftDiaLim, int rightDiaLim){
if(colLim == limit){
return 1;
}
// 所有候选皇后的位置,都在pos上
int pos = limit & (~(colLim | leftDiaLim | rightDiaLim));
int mostRightOne = 0;
int res = 0;
while(pos != 0){
mostRightOne = pos & (~pos + 1);
pos = pos - mostRightOne;
res += process2(limit, colLim | mostRightOne, (leftDiaLim | mostRightOne) << 1, (rightDiaLim | mostRightOne) >> 1);
}
return res;
}
int totalNQueens(int n) {
return num2;
}
};