N皇后问题
N皇后问题的要求是什么?
- 不能同行
- 不能同列
- 不能同斜线
如何用回溯思想来思考该题(怎么用回溯来搜二维结构),N叉树是怎么构造的?
N皇后问题其实本质上是一个不断的选取位置并赋值的过程,遵循的顺序是:选位置→判断合法性→赋值Q→进入下一层递归→递归退出进行回溯,选当前层的后一个位置。其实回溯法主打的就是一个选元素的过程,就看选的合不合法了。接下来看N叉树怎么构造的,以n=3为例
递归深度为n,递归宽度也是n
什么时候终止递归??
在不停的往下递归的时候,直到最后一层的皇后放入之后,说明这个时候可以将结果收集起来,这个时候行数是等于n的,为什么?因为我们的行数参数刚开始传进去为0,也就是说一进入递归,实际正在操作的行数要比定义的row大1,那为什么不直接传入1呢,这是为了方便用二维索引来取值,传入1当然是可以的,只不过在取值时行索引要减1(数组索引操作总是从0开始)。然后判断当前位置是不是一个合法的位置,接着就是进行递归了。那说明如果row等于了n,那这个时候实际操作递归的深度为第n+1层,这个时候的path其实还停留在第n层(因为在进入一层新的递归刚开始的时候并没有对path做任何操作,仅仅只是判断合法性而已,所以上一层递归对path的操作结果到未进入本层递归中的循环之前都是不变的),正符合条件,说明加入结果集的时候就是这个时候,因为这个时候第n+1层的递归还没开始呢。代码如下:
//当到达叶子节点的时候就将path纳入到结果集
if(row == n){
//为什么是row==n而不是row-1==n呢
//因为row==n,说明此时最后一层肯定是填充皇后了,注意,是此时,此时,此时,这个时候row==n时还没进入第n+1层递归的循环中呢
result.push_back(path);
return;
}
需要检查的位置是哪些??
根据题目要求,不同行,不同列,不同斜线。那么对应的检查位置其实有八个方向,上下左右四个对角线,但其实要判断的位置没有这么多。我们知道,我们是一层一层的往下走的,说明上面层放置的皇后会影响下面的层,下面的层不会影响上面的层,所以,下以及左下和右下这两个方向可以舍去了,那左右呢?其实也可以舍去,因为在一层里面,一轮循环仅选一个位置,当选右边位置的时候,左边位置就会被置为逗点(这就是回溯的效果)。所以说左侧的位置也不会影响当前位置,那右边呢?因为循环是从左往右一个位置一个位置走的,当前位置还没结束,所以右边的肯定影响不到当前位置,所以左右两个位置也舍去。那剩下的就是左上角,上面,右上角,而这三者都具有一个共同点,那就是它们均处于当前层的上面层,所以这三个位置是需要进行检查的。
判断合法性位置代码怎么写??
bool isValid(int row,int col,int n,vector<string> &path){
/*
该函数检查棋盘中某一位置是否合法,该函数非常重要
*/
//检查同一列的多个行
for(int i = 0; i < row; ++i){
if(path[i][col] == 'Q'){
return false;
}
}
//检查右上45度角,j < n这个条件非常重要
for(int i = row - 1,j = col + 1; i >= 0&&j < n; --i,++j){
if(path[i][j] == 'Q'){
return false;
}
}
//检查左上135度角,j >= 0这个条件也很重要
for(int i = row - 1, j = col - 1; i >= 0&&j >= 0; --i,--j){
if(path[i][j] == 'Q'){
return false;
}
}
return true;
}
注意,代码中的 j < n以及 j ≥ 0非常重要,j < n这是在搜索右上角皇后时需要加入的一个限定条件,因为j不能跑到棋盘右边界外面去。同理 j ≥ 0是搜索左上角时加入的条件,j也不能跑到棋盘左边界外面去。
用来保存每一层递归结果的变量放在哪儿?
在调用函数中定义,为什么不是全局变量呢,因为该题的示例中,对于那些无效的位置都是逗点符号,说明这是有初始化的棋盘,况且这个棋盘的大小是根据函数中的n来决定的,定义为全局变量的话,n只是一个形参不是类成员,那怎么获取n呢,所以得在函数调用中定义。
单层递归逻辑怎么写??(重点)
在这里对于某一个位置的合法性判断使用到了一个技巧:另外写一个函数来判断当前棋盘位置是否合法,这个函数的实现非常重要,在这个函数的实现过程中,要明确一点:在C/C++中字符串的本质是一个字符数组。想到这一点,在判断的时候才会由此想到对vector这样的结构进行二维索引取值操作。单层逻辑就是如果位置合法,那么就赋值Q,进入深度递归,递归退出后就回溯,进入下一轮循环,如果直接不合法直接也是下一轮循环。
bool isValid(int row,int col,int n,vector<string> &path){
/*
该函数检查棋盘中某一位置是否合法,该函数非常重要
*/
//检查同一列的多个行
for(int i = 0; i < row; ++i){
if(path[i][col] == 'Q'){
return false;
}
}
//检查右上45度角,j < n这个条件非常重要
for(int i = row - 1,j = col + 1; i >= 0&&j < n; --i,++j){
if(path[i][j] == 'Q'){
return false;
}
}
//检查左上135度角,j >= 0这个条件也很重要
for(int i = row - 1, j = col - 1; i >= 0&&j >= 0; --i,--j){
if(path[i][j] == 'Q'){
return false;
}
}
return true;
}
void backTracking(int n,int row,vector<string> &path){
//当到达叶子节点的时候就将path纳入到结果集
if(row == n){
//为什么是row==n而不是row-1==n呢
//因为row==n,说明此时最后一层肯定是填充皇后了,注意,是此时,此时,此时,这个时候row==n时还没进入下一层递归呢
result.push_back(path);
return;
}
for(int i = 0; i < n; ++i){
if(isValid(row,i,n,path)){
//位置合法,则将皇后放到该位置
path[row][i] = 'Q';
//递归下一层,也就是进入下一行
backTracking(n,row + 1,path);
//要么得到一种合理的方案递归退出,要么就是当前这一行所有位置均不合法退出递归,
//这个时候,就要回溯,将原先位置上的皇后变为逗点符号,因为无论是合理方案退出还是不合法退出都是由于上一层递归的位置上赋值后所导致,所以恢复原样进入下一轮循环,再进行判断。
path[row][i] = '.';
}
}
}
初始化语句怎么写?
在本题中,题目只给了一个n,没有给定棋盘的数据结构,那么肯定得自己创建,创建的话之前说过要放在调用函数内部。但是怎么初始化呢??首先这个结果集我们定义出来了(结果集不是棋盘,结果集里面的每个元素抽象出来才是棋盘),然后要做的其实就是将一个一个符合条件的棋盘放到这个结果集中,那这么说其实结果集不用初始化了,因为它的值完全是靠n行组成的vector的结果来定的。现在来看每一行的结果,其实每一行的结果就是一个字符串(一维字符数组),而多个字符串纵向堆叠则形成了一个棋盘结果(二维字符数组),多个棋盘结果形成了题目中返回的数据。所以现在就是要将这个每一行的结果给确定出来,那这个结果的话就是一个字符串,包含了n个字符,在这一行中仅有一个字符是Q,其他的全是逗点符号,所以这个结果一开始要进行初始化,每个值要初始化为逗点符号。如果不初始化的话,如果说某个字符为Q,此时当前行后面的位置可以不用再看了,因为一行只能放一个皇后,但是它后面的字符呢??全为NULL,根据示例输出这显然这是不符合结果的。所以要将棋盘(代码中的path变量)进行初始化为n个string。为什么是初始化path呢,在这里其实path就是一个棋盘,因为我们要对每个位置判断是否合法然后才往里面填值,那些不符合条件的自然舍去。
整体代码
class Solution {
public:
//定义全局变量结果集
vector<vector<string>> result;
bool isValid(int row,int col,int n,vector<string> &path){
/*
该函数检查棋盘中某一位置是否合法,该函数非常重要
*/
//检查同一列的多个行
for(int i = 0; i < row; ++i){
if(path[i][col] == 'Q'){
return false;
}
}
//检查右上45度角,j < n这个条件非常重要
for(int i = row - 1,j = col + 1; i >= 0&&j < n; --i,++j){
if(path[i][j] == 'Q'){
return false;
}
}
//检查左上135度角,j >= 0这个条件也很重要
for(int i = row - 1, j = col - 1; i >= 0&&j >= 0; --i,--j){
if(path[i][j] == 'Q'){
return false;
}
}
return true;
}
void backTracking(int n,int row,vector<string> &path){
//当到达叶子节点的时候就将path纳入到结果集
if(row == n){
//为什么是row==n而不是row-1==n呢
//因为row==n,说明此时最后一层肯定是填充皇后了,注意,是此时,此时,此时,这个时候row==n时还没进入下一层递归呢
result.push_back(path);
return;
}
for(int i = 0; i < n; ++i){
//位置合法,则将皇后放到该位置
path[row][i] = 'Q';
//递归下一层,也就是进入下一行
backTracking(n,row + 1,path);
//要么得到一种合理的方案递归退出,要么就是当前这一行所有位置均不合法退出递归,
//这个时候,就要回溯,将原先位置上的皇后变为逗点符号,因为无论是合理方案退出还是不合法
//退出都是由于上一层递归的位置上赋值后所导致,所以恢复原样进入下一轮循环,再进行判断。
path[row][i] = '.';
}
}
vector<vector<string>> solveNQueens(int n) {
//清除结果集空间
result.clear();
//最开始的时候要初始化棋盘
vector<string> path(n,string(n,'.'));
//最初的row为0
backTracking(n,0,path);
return result;
}
};