N皇后问题
【问题描述】N皇后问题从八皇后问题延伸而来。八皇后问题是19世纪著名的数学家高斯于1850年提出的。问题是在 8×8 的国际象棋棋盘上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列 或同一斜线上。所以N皇后问题即在 n×n 的棋盘上摆放 n 个皇后,使任意两个皇后都不能处于同一行、同一列或同一斜线上。
【算法思路】根据题目要求的摆放规则可知,棋盘的每一行可以并且必须摆放一个皇后,所以, n
皇后的可能解用一个 n 元向量表示,即第 i 个皇后摆放在第 i 行第 xi 列的位置,由于两个皇后不能位于同一列,所以, n 皇后问题的解向量必须满足约束条件。
可以将N皇后问题的n×n 棋盘看成是矩阵,设皇后 i
和皇后 j 的摆放位置分别是 i, xi 和 (j, xj) ,则在 棋盘上斜率为-1的同一条斜率上,满足条件 i-xi=j-xj ,在棋盘上斜率为1的同一条斜线上,满足条件 i+xi=j+xj 。如图所示,综合两种情况,N皇后问题的解必须满足约束条件:i-j=|xi-xj| 。
为了简化问题,下面使用四皇后问题来具体进行讨论。回溯法从空棋盘开始,依次在每一行寻找一格位置放置一个皇后,具体过程如下图5-4所示:
在上图中每一个子图表示每一步的搜索,其中“Q” 表示放置一个皇后,“×”表示一个不合法的放置或者一次失败的尝试,对每一步的具体说明如下:
(a) 首先把皇后1摆放到它所在行的第一个位置,也就是第一行第一列。
(b) 对于皇后2,将其放置第1列后,与现有的放置进行判断,发现皇后2与皇后1位于同一列,因此该放置不合法;同理放在第2列将会与皇后1位于同一对角线上,也不合法。在经过第1列和第2列的尝试后,摆放到第2行第3列。
(c) 尝试在第3行放置皇后3,第1列发现与皇后1同列,第2列与皇后2同对角线,第3列与皇后2同列,第4列同样与皇后2同对角线。最后皇后3无法放置到第3行的任何一列,因此结束该层搜索,回溯到上一层。
(d) 回溯到第2行,并且没有得到可行解,说明当前的放置会导致无解。我们将皇后2原先位于第2行第3列的摆放标记为失败尝试,并将皇后2放置到下一个位置,第2行第4列。
(e) 再次尝试放置皇后3,第1列会与皇后1同列,第2列没有问题,于是将皇后3摆放到第3行第2列。
(f) 尝试在最后一行放置皇后4,发现第1列会与皇后1同列并且与皇后3同对角线,第2列与皇后3同列,第3列与皇后3同对角线,第4列与皇后2同列。因此皇后4没有有效的放置。结束搜索并回溯到上一层。
(g) 回到皇后3,将原本第2列的放置标记为无效。继续搜索,发现第3列与皇后2同对角线,第4列与皇后2同列。因此皇后3也无解,结束搜索回溯到上一层。
(h) 回到皇后2,将原本第4列的位置标记为无效。但当前已经搜索到最后一列,没有更多的位置可以尝试。结束搜索回到上一层。
(i) 回到第1行的皇后1,意味着第1行第1列的摆放是无解的,将该位置标记为失败尝试。将皇后1放置到下一个位置,第1行第2列,再次往下进行搜索。
(j) 在第2行放置皇后2,摆放在第1、2、3列均会与皇后1同列或者同对角线,最后摆放到第2行第4列。
(k) 在第3行放置皇后3,摆放到第1列并不会造成其他两个皇后的冲突,直接下一层。
(l) 在最后一行摆放皇后4,第1列与第2列均会和现有的皇后同列造成冲突,最后摆放到第3列。
到了第(l)步为止我们就得到了四皇后问题的一个有效解,如上图的最后一张子图所示。
在看到上述的搜索过程之前,可能有同学会觉得整个搜索量会不会非常大,因为每一个皇后都有4种放置可能,加起来就应该是总共有 4^4=256 种情况。但实际过程演示下来发现并没有那么多,这是因为在中间过程中很多的搜索都因为同列或者同对角线冲突而中止,并不会进入下一层的搜索,这便是回溯算法中很重要的回溯剪枝技巧。
【算法描述】了解到上述具体步骤后,我们进一步总结n皇后的求解算法描述如下:
回溯法求解n皇后问题
输入:皇后的个数n
输出:n皇后问题的解x[n]
1. 初始化解向量x[n]={-1};
2. k=1;
3. while(k>=1) 重复执行以下步骤4-8:
4. 把皇后k摆放在下一列的位置,即x[k]++;
5. 从x[k]开始依次考察每一列,如果皇后k摆放在x[k]位置不发生冲突,则转步骤6; 否则x[k]++试探下一列;
6. 若n个皇后已全部摆放,则输出一个解,算法结束;
7. 若尚有皇后没摆放,则k++,转步骤3摆放下一个皇后;
8. 若x[k]出界,则回溯,x[k]=-1,k--,转步骤3重新摆放皇后k;
4. 退出循环,说明n皇后问题无解;
【算法实现】算法实现代码如下:(Java版本)
import java.util.Arrays;
public class NQueenSolution {
// 回溯法求解n皇后问题
public int queen(int n) {
// x[i]表示第i行放置皇后的列下标,-1表示未放置
int[] x = new int[n];
Arrays.fill(x, -1);
int k = 0, count = 0;
// 循环模拟递归,摆放皇后k
while (k >= 0) {
x[k]++;
while (x[k] < n && checkPlace(k, x)) {
x[k]++;
}
if (x[k] < n && k == n - 1) {
for (int j = 0; j < n; j++)
System.out.print(x[j] + 1 + " ");
System.out.println();
++count;
}
if (x[k] < n && k < n - 1) k = k + 1;
else x[k--] = -1;
}
// 最后返回总共有多少组解
return count;
}
// 检查皇后k放置在x[k]列是否发生冲突
// 返回true表示产生冲突,不能放置
// 返回false表示可以放置在该位置
public boolean checkPlace(int k, int[] x) {
for (int i = 0; i < k; i++) {
if (x[i] == x[k] ||
Math.abs(i - k) == Math.abs(x[i] - x[k])) {
return true;
}
}
return false;
}
public static void main(String[] args) {
int ans = new NQueenSolution().queen(4);
System.out.println("共有 " + ans + " 个解!");
}
}
Python3的代码实现如下:
# 回溯法求解n皇后问题
def queen(n: int) -> int:
# x[i]表示第i行放置皇后的列下标,-1表示未放置
x, k, count = [-1] * n, 0, 0
# 循环模拟递归,摆放皇后k
while k >= 0:
x[k] += 1
while x[k] < n and any(x[i] == x[k] or abs(i - k) == abs(x[i] - x[k]) for i in range(k)):
x[k] += 1
if x[k] < n and k == n - 1:
print(' '.join(map(str, x)))
count += 1
if x[k] < n and k < n - 1:
k = k + 1
else:
x[k] = -1
k -= 1
# 最后返回总共有多少组解
return count
# mian
ans = queen(4)
print('共有', ans, '个解!')
回溯法总结(具体参考课本及知识点总结)
- 基本概念(解空间、解空间树、活节点、死节点等)
- 回溯法的简单理解就是通过DFS(或前序遍历)构建并搜索遍历解空间树的过程
- 优化:剪枝(去掉不必要的分支)/ 记忆化回溯(避免重复搜索,本质上即为动态规划)
- 排列树(时间复杂度为O(n!) 典型例题:全排列/N皇后)
- 子集树(时间复杂度O() 典型例题:01背包)