一、问题介绍
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:[["Q"]]
提示:1 <= n <= 9
皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。
二、问题分析
N皇后是回溯算法的经典问题。
那么什么是回溯算法呢?
其实回溯算法和深度优先搜索很像,实际上是一个决策树的遍历过程,
一般求:满足条件的路径有哪些,有多少条。
我们需要思考三个问题:
1.路径:已经做出的选择。
2.选择列表:当前可以做的选择。
3.结束条件:到达决策树底层,无法再做选择的条件。
回溯算法的基本框架和遍历一颗多叉树非常类似:
result = [];
def backtrack(路径,选择列表)
{
if 满足结束条件:
result.add(路径);
return ;
for 选择 in 选择列表
{
做选择;
backtrack(路径,选择列表);
撤销选择;
}
}
核心,就是for循环里的递归:我们在递归调用之前做选择,在递归调用后撤销选择。
那么回到我们的N皇后问题:
假设N = 8,也就是棋盘大小为8×8:
这是N = 8的一种放法。
我们先来研究一下三皇后:
根据回溯算法的原理,
首先,我们进行第一次backtrack调用,先在第一行的第一列位置放上第一个皇后;
然后对这个位置进行检查,看看是否有冲突——显然,第一个位置不会有任何冲突。
然后我们进行第二次backtrack函数调用,在第二行第一列位置放上一个皇后。
经过冲突检测,发现有冲突了,因为第一个皇后和第二个皇后连成了一条线,
所以此时产生冲突,也就无法继续调用backtrack将第三行的皇后进行摆放,
那么此时continue,进行第二次放置二行皇后的选择。
因为又冲突了,所以我们将第二行皇后放在第三列。
这时,因为放置成功,所以我们调用第三次backtrack,进入第三行。
因为第三行所有位置都无法放置,导致for循环全部进行完毕。
那么此时,回到了操作第二行的代码阶段。
第二行已经运行完了backtrack,也就是已经递归地将剩下所有行的情况都完成了,
那么我们需要再撤回它,回到第一行的第二轮放置中。
然后继续递归地调用backtrack,以Q在(1,2)为头,完成剩下的搜索。
所以我们的主函数backtrack应该这样来写:
void backtrack(vector<string>& board, int row)
{
//结束条件,此时已到达决策树底层
//每一行都成功放置了皇后,记录结果
if (row == board.size())
{
ans.push_back(board);
return;
}
//每一行的长度
int length = board[row].size();
//在当前行的每一列都可能放置皇后
for (int col = 0; col < length; col++)
{
//首先进行冲突检测,排除会和其他棋子互相攻击的格子
if (!isValid(board, row, col))
{
continue;
}
//开始做选择
board[row][col] = 'Q';
//进入下一行放皇后
backtrack(board, row + 1);
//撤销当前选择,在当前列的下一列放置新的皇后,
//相当于换一个首结点,然后继续遍历决策树
board[row][col] = '.';
}
}
而对于冲突检测,我们只需要对三个位置检查即可:
上方,左上,左下。
因为本行由于是for循环迭代放置皇后,所以本行左右无需判断;
因为皇后是从上至下放置,所以整个下方无需判断。
bool isValid(vector<string>& board, int row, int col)
{
//1.检测当前列的上方是否有皇后冲突
for (int i = 0; i < row; i++)
{
if (board[i][col] == 'Q')
{
return false;
}
}
//2.检测当前格的左上方是否有皇后冲突
for (int i = row - 1, j = col - 1;
i >= 0 && j >= 0;
i--, j--)
{
if (board[i][j] == 'Q')
{
return false;
}
}
//3.检测当前格的右上方是否有皇后冲突
for (int i = row - 1, j = col + 1;
i >= 0 && j < length;
i--, j++)
{
if (board[i][j] == 'Q')
{
return false;
}
}
//只需要检查三个位置。因为backtrack的约束,本行的左右不会有其他皇后
//而且无需检查皇后下方的格子,因为是从上至下摆放的。
return true;
}
最后放上总代码:
#include <stdio.h>
#include <iostream>
#include <vector>
using namespace std;
//存放最终结果的容器
vector<vector<string>> ans;
//是否可以在board[row][col]放置皇后?
bool isValid(vector<string>& board, int row, int col)
{
//1.检测当前列的上方是否有皇后冲突
for (int i = 0; i < row; i++)
{
if (board[i][col] == 'Q')
{
return false;
}
}
//2.检测当前格的左上方是否有皇后冲突
for (int i = row - 1, j = col - 1;
i >= 0 && j >= 0;
i--, j--)
{
if (board[i][j] == 'Q')
{
return false;
}
}
//3.检测当前格的右上方是否有皇后冲突
for (int i = row - 1, j = col + 1;
i >= 0 && j < length;
i--, j++)
{
if (board[i][j] == 'Q')
{
return false;
}
}
//只需要检查三个位置。因为backtrack的约束,本行的左右不会有其他皇后
//而且无需检查皇后下方的格子,因为是从上至下摆放的。
return true;
}
void backtrack(vector<string>& board, int row)
{
//结束条件,此时已到达决策树底层
//每一行都成功放置了皇后,记录结果
if (row == board.size())
{
ans.push_back(board);
return;
}
//每一行的长度
int length = board[row].size();
//在当前行的每一列都可能放置皇后
for (int col = 0; col < length; col++)
{
//首先进行冲突检测,排除会和其他棋子互相攻击的格子
if (!isValid(board, row, col))
{
continue;
}
//开始做选择
board[row][col] = 'Q';
//进入下一行放皇后
backtrack(board, row + 1);
//撤销当前选择,在当前列的下一列放置新的皇后,
//相当于换一个首结点,然后继续遍历决策树
board[row][col] = '.';
}
}
int solveNQueens(int n)
{
vector<string> board(n, string(n, '.'));
backtrack(board, 0);
return ans.size();
}
int main()
{
cout << solveNQueens(8);
return 0;
}
运行截图:
八皇后共有92种解法。