N皇后问题的经典不言而喻,不多哔哔,直接上
审题
(祝大家审题健康哈哈哈哈哈烂梗)
给定输入n----一个自然数
目标是给出一个让n个皇后们无法互相攻击的方案
也就是说,在一张二维n行n列矩阵中,同列同行同斜线都只能有一个皇后Q
给定的返回也是一个二维数组,其中皇后“Q”和空位"."可以通过字符串的基本操作来进行插入等等。
基本的思路就是上面了👆
分析
把它划给回溯法大类是有原因的。
正向思考:1.选中第i行的某个位置
2.选中第i+1行的某个位置,判断是否合法
3.合法继续,不合法更换位置,如果遍历了第i+1行的全部位置都不合法,则重选上一行(第i行)位置
这样理解是不是就很想组合问题了!
代码逻辑
就非常抱歉,这里还是参考我自己习惯的回溯法代码框架,有点死记硬背那味儿,但是逻辑上是有道理的
class Solution {
//1.结果数组直接用主函数返回值类型就对了
List<List<String>> res = new ArrayList<>();
//某种意义上的主函数
public List<List<String>> solveNQueens(int n) {
}
//回溯函数,传入参数是行数n,以及标记row,表示当前在第几行
void backtracking(n,int row){
}
//判断是否合法的函数
boolean isValid(){
}
}
主函数
在主函数的操作其实非常直观。
定义出一个新的棋盘,大小为n*n,类型是字符数组,并初始化用“.”填充
这里的几个基本的操作可以记下来,比如Arrays里的很多函数经常用得上(fill,sort等等)
//某种意义上的主函数
public List<List<String>> solveNQueens(int n) {
char[][] chessboard = new char[n][n];
for(char[] c : chessboard){
Arrays.fill(c,".");
}
backtracking(n,0);
return res;
}
因为我们需要操作的过程本质上是在对一个二维字符型数组做修改,但是注意返回类型中的每条“路径”是字符串类型的一维数组,所以这里给chessboard专门设置一个转换类型的方法(当然你有好方法也可以自己在初始化的时候就定义好相同类型,在递归时给出相应操作也可以,我这里基本上用的是随想录的源码)
//数据类型转换工具,二维字符数组转换为String类型的一维数组
public List Array2List(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}
判断合法的函数
由主函数中定义的棋盘不难看出,我们肯定还得是在棋盘内判断,并遵守以下规则:
1.同行不能出现两个皇后
2.同列不能出现两个皇后
3.同斜线不出现两个皇后
同时由于回溯的过程就是一行一行铺,在处理每行时,遍历所有列,并在此时判断当前位置下的皇后是否合法
//判断是否合法的函数
boolean isValid(int row,int col,char[][] chessboard,int n){
for(int i =0;i < row;i++){
//同列有Q则返回错误
if(chessboard[i][col]=='Q'){
return false;
}
}
//检查右上到左下斜线
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-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
回溯过程
有了以上的铺垫,回溯过程的思路就相对来说清晰明了:
1.以行为递进单位,判断在当前行,选中第i列时是否合法
2.若合法,则进入下一行;不合法则继续遍历第i+1列
3.若当前行所有列都不合法,则进行回溯
4.若遍历到最后一行,并找到合法方案,则将当前方案纳入结果集,再继续回溯。
所以递归出口就是,当前行已经走到了最后一行:
if(row == n){
res.add(Array2List(chessboard));
return;
}
回溯函数的循环体内:
1.以列为递进单位
2.判断合法则修改chessboard;不合法则continue(默认就是进入下一次循环,也就是下一列)
3.递归至下一行
4.遇到出口,则回溯
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] = '.';
}
}
以上是代码的全部模块,贴个所有代码:
class Solution {
//1.结果数组直接用主函数返回值类型就对了
List<List<String>> res = new ArrayList<>();
char [][] chessboard;
//某种意义上的主函数
public List<List<String>> solveNQueens(int n) {
char[][] chessboard = new char[n][n];
for(int i =0;i<n;i++){
for(int j = 0;j <n;j++){
chessboard[i][j]='.';
}
}
backtracking(n,0,chessboard);
return res;
}
//回溯函数,传入参数是行数n,以及标记row,表示当前在第几行
void backtracking(int n,int row,char[][]chessboard){
if(row == n){
res.add(Array2List(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] = '.';
}
}
}
//数据类型转换工具,二维字符数组转换为String类型的一维数组
public List Array2List(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}
//判断是否合法的函数
boolean isValid(int row,int col,char[][] chessboard,int n){
for(int i =0;i < row;i++){
//同列有Q则返回错误
if(chessboard[i][col]=='Q'){
return false;
}
}
//检查右上到左下斜线
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-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
}
总结:
1.N皇后的遍历思维符合回溯递归逻辑
2.如果是有这样的固有框架思维,需要考虑的仅仅是一些细节:
1)判断合法
2)过程集合的数据类型
3)回溯过程中的思路要清晰