n皇后问题-回溯法求解
1.算法描述
在n×n格的国际象棋上摆放n个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
n皇后是由八皇后问题演变而来的。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。
2.算法分析
随着计算机的普及和发展,以前人们无法解决的问题,计算机可以简单计算出来。而且思路十分清晰,那就是暴力求解,遍历所有情况,然后计算出解的个数。
2.1 回溯法
按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法
2.2 回溯法思路
用数组模拟棋盘,从第一行开始,依次选择位置, 如果当前位置满足条件,则向下选位置, 如果不满足条件,那么当前位置后移一位。
最后一个不满足,回溯到上一行, 选下个位置,继续试探。
其实并不需要一个n*n的数组,我们只需要一个n长度的数组来存位置。
表示方式: arr[i] = k; 表示: 第i行的第k个位置放一个皇后。这样一个arr[n]的数组就可以表示一个可行解, 由于回溯,我们就可以求所有解。
2.3 n皇后回溯求解
因为八皇后不能在同行,同列, 同斜线。
- 每一行放一个皇后,就解决了不在同行的问题。
- 在第i行的时候,遍历n列,试探位置。和之前所有行放的位置进行比较。
- 比较列:当前列col 不等于 之前 所有列。 即col != arr[i].
- 比较斜线, 因为不再同一斜率为1或者-1的斜线。(row - i) / (col - arr[i]) != 1 或 -1
可以取巧用绝对值函数: abs(row-i) != abs(col-arr[i])
我们可以提取比较方法:
1 | public boolean comp(int row, int col){ //当前行和列 |
比较函数写完后, 就只剩下回溯过程, 我们采用递归的方式回溯,比较好理解。
1 | //当前层 row |
2.4 时间复杂度
递归n行, 每次循环 n 列, 比较 0~n-1 次。
n * n * (n - 1) / 2.
也就是 O(n^3). 哇塞,真暴力。
2.5 空间复杂度
因为只用了arr[n]的数组,也就是O(n).
3.代码实现
1 | public class NQueen { |
4. 拓展,位运算+回溯法实现
虽然计算机算的很快,但是上诉方法实在是太慢了, java就更慢了。如何网上就有大佬给出了位运算求解。精妙的不行。
4.1 算法思路
我们不再用数组来存储位置,而是用一个整数k,k一开始等于0. 不是普通的0.我们也不比较了,直接用两个整数l和r 记录在斜线在当前行不能走的位置。如果是n皇后, 那么用一个整数
nn = 1 << n 表示结束。
举个栗子吧: 8皇后问题。
初始化 那么我们就变成二进制的角度来看这些初始化的数据吧。
k = 00000000, l = 00000000, r = 00000000; nn = 11111111; (<- 8个0 8个1)
- k: 每个位置i的0表示没有皇后,1表示在第i个位置放了一个皇后。
- l: 0表示之前所有的列中放的皇后斜率为-1的线上没有涉及这个位置, 1 表示涉及到了,不能放皇后
- r: 同l, 所有斜率为1的线涉及的位置。
l和r的实现:
比如k = 00110001. 我要在第4个为位置放一个皇后, 假设l和r都没有涉及这个位置。
那么这个位置x= 00001000.
- k = (k & x) = 00111001.
- l = (l & x) << 1.
- r = (r & x) >> 1.
假设l = 00110001, r = 00100010.下一行,l表示斜率为-1不能放的位置, 那么第i+1行 l 中所有为1的数字都需要向左移动一位,r需要向右移动一位。 l & x 也就是加上当前选中的位置一起移动。
4.2代码实现
1 | //k 当前已走了多少个位置。 l 左斜线不能走的位置, r 右斜线不能走的位置。 |
4.3 算法分析
- 时间复杂度,遍历n行, 每行n列. O(n^2). emmm 快了不少。
- 空间复杂度,只用了几个变量, O(1).
5. 展望
其实还有其他方式和更快的方式求解,比如位运算+多线程, 还有号称时间复杂度为O(1),利用数学公式的构造法求解。扶我起来,我要继续学。