问题
国际象棋中的皇后,可以横向、纵向、斜向移动。如何在一个NXN的棋盘上放置N个皇后,使得任意两个皇后都不在同一条横线、竖线、斜线方向上?
举个栗子,下图的绿色格子是一个皇后在棋盘上的“封锁范围”,其他皇后不得放置在这些格子:
递归回溯法
所谓递归回溯,本质上是一种枚举法。这种方法从棋盘的第一行开始尝试摆放第一个皇后,摆放成功后,递归一层,再遵循规则在棋盘第二行来摆放第二个皇后。如果当前位置无法摆放,则向右移动一格再次尝试,如果摆放成功,则继续递归一层,摆放第三个皇后……
如果某一层看遍了所有格子,都无法成功摆放,则回溯到上一个皇后,让上一个皇后右移一格,再进行递归。如果八个皇后都摆放完毕且符合规则,那么就得到了其中一种正确的解法。
看个例子:把皇后
1.第一层递归,尝试在第一行摆放第一个皇后:
2.第二层递归,尝试在第二行摆放第二个皇后(前两格被第一个皇后封锁,只能落在第三格):
3.第三层递归,尝试在第三行摆放第三个皇后(前四格被第一第二个皇后封锁,只能落在第五格):
4.第四层递归,尝试在第四行摆放第四个皇后(第一格被第二个皇后封锁,只能落在第二格):
5.第五层递归,尝试在第五行摆放第五个皇后(前三格被前面的皇后封锁,只能落在第四格):
6.由于所有格子都“绿了”,第六行已经没办法摆放皇后,于是进行回溯,重新摆放第五个皇后到第八格。:
7.第六行仍然没有办法摆放皇后,第五行也已经尝试遍了,于是回溯到第四行,重新摆放第四个皇后到第七格。
8.继续摆放第五个皇后,以此类推……
看个例子:四皇后
现在我们把第一个皇后放在第一个格子,被涂黑的地方是不能放皇后的
第二行的皇后只能放在第三格或第四格,比如我们放在第三格:
这样一来前面两位皇后已经把第三行全部锁死了,第三位皇后无论放在第三行的哪里都难逃被吃掉的厄运。于是在第一个皇后位于第一格,第二个皇后位于第三格的情况下此问题无解。所以我们只能返回上一步,来给2号皇后换个位置:
此时,第三个皇后只有一个位置可选。当第三个皇后占据第三行蓝色空位时,第四行皇后无路可走,于是发生错误,则返回上层调整3号皇后,而3号皇后也别无可去,继续返回上层调整2号皇后,而2号皇后已然无路可去,则再继续返回上层调整1号皇后,于是1号皇后往后移一格位置如下,再继续往下安排:
建模:抽象成N叉树
从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。
那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。
题目
51. N皇后:所有的解决方案
题目来源
题目描述
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
}
};
题目解析
(1)递归函数参数
- 定义两个全局二维遍历result来记录最终结果
- 参数n是棋盘的大小,row表示当前遍历到棋盘的第几层
vector<vector<string>> result;
void backtracking(int n, int row, vector<string>& chessboard) {
(2)递归函数参数
- 从上面可以看出,当递归到棋盘的最底层(也就是叶子节点)时,就可以收集结果并返回了
if (row == n) {
result.push_back(chessboard);
return;
}
(3)单层搜索的逻辑
- 递归深度就是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] = '.'; // 回溯,撤销皇后
}
}
(4)验证是否合法
- 皇后约束:
- 不能同行
- 不能同列
- 不能同斜线 (45度和135度角)
代码如下:
class Solution {
vector<vector<string>> ans;
bool isValid(int row, int col, vector<string>& chessboard, int n) {
int count = 0;
// 检查列
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;
}
void dfs(int n, int row, std::vector<std::string> &chessboard){
if(row == n){
ans.push_back(chessboard);
return;
}
for (int col = 0; col < n; ++col) {
if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
chessboard[row][col] = 'Q'; // 放置皇后
dfs(n, row + 1, chessboard);
chessboard[row][col] = '.'; // 回溯,撤销皇后
}
}
}
public:
vector<vector<string>> solveNQueens(int n) {
std::vector<std::string> chessboard(n, std::string(n, '.'));
dfs(n, 0, chessboard);
return ans;
}
};
为什么没有在同行进行检查呢?因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。
52. N皇后 II:解决方案有几种
题目来源
题目描述
class Solution {
public:
int totalNQueens(int n) {
}
};
题目解析
暴力递归
(1)思路
- 如果在(i, j)位置(第i行第j列)放置了一个皇后,接下来哪些位置不能放皇后呢?
- 整个第i行都不能放皇后
- 整个第j列都不能放皇后
- 如果位置(a, b)满足|a - i| == |b - j|,说明(a, b)与(i, j)处在同一条斜线上,也不能放皇后
(2)实现
- 把递归过程直接设计成逐行放置皇后的方式,可以避开条件1的那些不能放置的位置
- 用一个数组保存已经放置的皇后位置,假设数组为record,
r
e
c
o
r
d
[
i
]
record[i]
record[i]的值表示第
i
i
i行皇后所在的列数。在递归计算到第i行第j列时:
- 先看 r e c o r d [ 0... k ] ( k < i ) record[0...k](k < i) record[0...k](k<i)的值,看是否有 j j j相等的值,如果有,说明不能放置皇后;
- 再看是否有 ∣ k − i ∣ = = ∣ r e c o r d [ k ] − j ∣ |k - i| == |record[k] - j| ∣k−i∣==∣record[k]−j∣,如果有,也不能放置皇后
(3)代码
class Solution {
bool isValid(std::vector<int>& record, int i, int j){
for (int k = 0; k < i; ++k) {
if(record[k] == j || std::abs(k - i) == std::abs(record[k] - j)){
return true;
}
}
return false;
}
// 当前来到i行,一共是0~N-1行
// 在i行上放皇后,所有列都尝试
// 必须要保证跟之前所有的皇后不打架
// int[] record record[x] = y 之前的第x行的皇后,放在了y列上
// 返回:不关心i以上发生了什么,i.... 后续有多少合法的方法数
int process(int i, std::vector<int>& record, int n){
if(i == n){
return 1;
}
int way = 0;
for (int j = 0; j < n; ++j) { //每个皇后有N种选择
if(isValid(record, i, j)){ //每次放之前都看一下这个位置能不能放
record[i] = j;
way += process(i + 1, record, n);
}
}
return way;
}
public:
int ways(int n){
if(n < 1){
return 0;
}
std::vector<int> record(n);
return process(0, record, n);
}
};
还可以使用位运算来加速(最优解)。
class Solution {
// 7皇后问题
// limit : 0....0 1 1 1 1 1 1 1,这个变量的值在递归过程中是始终不变的
// 之前皇后的列影响:colLim,即递归计算到上一行为止,在哪些列上已经放置了皇后
// 之前皇后的左下对角线影响:leftDiaLim。举个例子
// 之前皇后的右下对角线影响:rightDiaLim
int process2(int limit, int colLim, int leftDiaLim, int rightDiaLim) {
if (colLim == limit) {
return 1;
}
// pos中所有是1的位置,是你可以去尝试皇后的位置
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,
(unsigned)(rightDiaLim | mostRightOne) >> 1); //无符号右移
}
return res;
}
public:
int ways(int n){
// 因为本方法位运算的载体是int型变量,所以该方法只能算1~32皇后问题,如果想要更多皇后,需要使用包含更多位的变量
if (n < 1 || n > 32) {
return 0;
}
// limit 表示当前行哪些位置可以放皇后,1表示可以放皇后,0表示不可以放皇后
// 比如8皇后, 初始化limit为0000000.....000011111111(limit 最右8个1,其他都是0)
// 比如32皇后,初始化limit为111111...1111(32个1)
int limit = n == 32 ? -1 : (1 << n) - 1;
std::vector<int> record(n);
return process2(0, record, n);
}
};
回溯法 + set集合
(1)思路
- 正对角线(从左上到右下)的特点:横纵坐标之差相同
- 副对角线(从右上到左下)的特点:横纵坐标之和相同
- 每行、每列及正副对角线上只能出现一个皇后,由于从上到下放置皇后,所以只需考虑当前坐标对应的列和正副对角线是否已被访问即可
(2)定义变量
- i:横坐标
- j:纵坐标
- col:set集合,存储遍历过的列j
- pos:set集合,存储正对角线之差i-j
- neg:set集合,存储副对角线之和i+j
(3)步骤
- 做选择:如果该坐标上对应的列和正副对角线上没有皇后,放置皇后并在对应集合中标记
- 递归:进入下一行
- 撤销选择:将当前标记从三个集合中删除
(4)复杂度分析
- 时间复杂度:O(N!)
- 空间复杂度:O(N)
(5)代码
class Solution {
int count = 0;
std::map<int, int> map1; //正斜线是否被占用
std::map<int, int> map2; //反斜线是否被占用
std::vector<int> col; //列是否被占用
void dfs(int i, int n){
if(i == n){ //放完了所有的皇后
++count;
return;
}
for (int j = 0; j < n; ++j) { //放第idx行,第j列
if(col[j] == 0 && map1.count(i - j) == 0 && map1.count(i + j) == 0){ //放第n个皇后,第j列是否可放?
col[j] = 1;
map1[i - j] = 1;
map1[i + j] = 1;
dfs(i + 1, n); //递归放下一个皇后
col[j] = 0;
map1[i - j] = 0;
map1[i + j] = 0;
}
}
}
public:
int totalNQueens(int n) {
if(n <= 1){
return n;
}
col.resize(n);
dfs(0, n);
return count;
}
};
回溯法+位运算
(1)思路
- N个位置可以对应成N个二进制位
- 二进制位状态:
- 0:无皇后,可以选择
- 1:有皇后,不可选择
- 比如八皇后第一行状态为
0000 0000
,当第二位被选择后该行的状态变成了0100 0000
,下一行同样第二位不能被选,正对角线对应的第三位不能被选(对应当前行右移了一位),副对角线对应的第一位不能被选(对应当前行左移了一位)- 已选列的二进制表示:0100 0000
- 已选正对角线的二进制表示:0010 0000
- 已选副对角线的二进制表示:1000 0000
(2)定义变量
- i:横坐标
- j:纵坐标
- col:已选列的二进制位
- pos:已选正对角线的二进制位
- neg:已被副对角线的二进制位
(3)步骤
- 将col、pos和neg做或运算(|)得到的二进制位pre中,1表示不能被选的位置,0表示可以被选的位置
- 遍历N位二进制,如果当前可被选择,则将该位置加入到对应二进制中,同时正对角线pos右移一位,副对角线neg左移一位
- 递归执行上述操作,直到所有选择执行完成
(4)复杂度分析
- 时间复杂度:O(N!)
- 空间复杂度:O(N)
(5)代码
面试题 08.12. N皇后:打印所有的方案
题目来源
题目描述
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
}
};
题目解析
思路:我们把这个问题分成n个阶段,依次将n个棋子放到第一行、第二行、第三行…。在放置的过程中,我们不停的检查当前的方法是否满足要求。如果满足,就跳到下一行继续放置棋子;如果不满足,就换一种方法,继续尝试
可视化过程
分析
解决八皇后问题,可以分为两个层面:
1.找出第一种正确摆放方式,也就是深度优先遍历。
2.找出全部的正确摆放方式,也就是广度优先遍历。
我们先来找出第一种正确摆放方式。
先解决几个问题
(1)国际象棋的棋盘如何表示?
用一个长度为9的二维数组即可
const int MAX_NUM = 8;
const chessBoard[] = new int[MAX_NUM][MAX_NUM]
由于这里使用的是int数组,int的初始值是0,代表没有落子。当有皇后放置的时候,对应的元素值改为1。
在这里,二维数组的第一个维度代表横坐标,第二个维度代表纵坐标,并且从0开始。比如chessBoard[3][4]代表的是棋盘第四行第五列格子的状态。
golang实现
package main
import "fmt"
const N = 4;
// 第1个N是由几行, 第2个N是有几列
var queue [N][N]int
func show() {
for i := 0; i < N; i++ { // 第i行
for j := 0; j < N ; j++ {
//fmt.Printf("%5d", queue[i][j])
if queue[i][j]==1{
fmt.Printf("%5s","■")
}else{
fmt.Printf("%5s","□")
}
}
fmt.Println()
}
fmt.Println()
}
// 判断 第row行,第col列是否可以存放数据
func judge(row, col int) bool {
for i := 0; i < col; i++ { //当前列是否已经摆放皇后
if queue[row][i] == 1 { // 行固定, 前j列是否已经存放数据
return false
}
}
for j := 0; j < col; j++{ //当前行是否已经摆放皇后
if queue[row][j] == 1 { // 列固定, 前j行是否已经存放数据
return false
}
}
// 左上角是否已经摆放皇后
for i, j := row - 1, col - 1; i >= 0 && j >= 0 ; i, j = i - 1, j - 1{
if queue[i][j] == 1 {
return false
}
}
// 右上角是否已经摆放皇后
for i, j := row - 1, col + 1; i >= 0 && j <= N - 1 ; i, j = i - 1, j + 1{
if queue[i][j] == 1 {
return false
}
}
// 左下角是否已经摆放皇后
for i, j := row + 1, col - 1; i <= N - 1 && j >= 0 ; i, j = i + 1, j - 1{
if queue[i][j] == 1 {
return false
}
}
//右下角是否已经摆放皇后
for i, j := row - 1, col + 1; i >= 0 && j <= N - 1 ; i, j = i - 1, j + 1{
if queue[i][j] == 1 {
return false
}
}
return true
}
var count = 0;
/*
* num 的意思是:我们正在放置第i个皇后
*/
func find_queue(num int) {
//show()
if num == N { // 所有的皇后都放完了
count++ // 那自然是找到了一种解法,于是八皇后问题解法数加1
show()
return
}
// 如果当前还没排到第八行,则遍历所有列col,将当前col存储在数组c里,然后使用judge()检查row行col列能不能摆皇后,若能摆皇后,则递归调用queen去安排下一列摆皇后的问题。
for row := 0;row < N ; row++ { // 第num个皇后放到第num列[固定好的],[0, N)行上试探
if judge(row, num) { // 判断当前位置是否可以摆放皇后
queue[row][num] = 1 //摆放皇后
find_queue(num + 1) //摆放下一个皇后
queue[row][num] = 0 // 只有在皇后摆不下去的时候会执行清0的动作(避免脏数据干扰),如果皇后摆放很顺利的话从头到尾是不会走这个请0的动作的,因为已经提前走if里面的return方法结束了。
}
//show()
}
}
func main() {
find_queue(0)
}