八皇后问题
八皇后问题是一个古老而经典的问题
- 在
8 x 8
的国际象棋盘上摆放8
个皇后,使其不能相互共计,任意两个皇后都不能处于同一行、同一列和同一斜线上- 问:共有多少种摆法
Leetcode题目
51.N皇后Ⅰ
52.N皇后Ⅱ
解决思路
暴力法
在64个格子中,任意选出8个格子,检查每一种摆法的可行性
- 根据排列组合,共有C 64 8 种组合,共
4.4 * 10 ^ 9
种,即44亿
回溯法
四皇后问题
在解决八皇后问题之前,可以先通过缩小数据规模的方式,看看如何解决四皇后问题
- 蓝色:可摆放
- 绿色:已摆放
- 黑色:不能摆放
- 红色箭头:回溯
编程实现
对于二叉树、图等数据结构,以
节点
为单位,类比到这里,对于八皇后问题,以行
为单位定义一个数组
cols
,索引
是行号,元素
是列号,用来记录皇后摆放的位置
- 例如
cols[4] = 5
表示,第五行的皇后在第六列定义一个检查规则的方法
boolean isValid(int 行, int 列)
,用于检查每个具体格子符不符合国际象棋规则,即检查它能不能被剪枝
- 遍历之前所有行,查看每一行摆放皇后所在的列,如果正好等于当前列,则立刻返回
false
- 当前行是全新的一行,所以不用在
行
维度上检查- 检查斜线:有多种方法
- 斜率法:
行坐标 - 列坐标
结果相等的,在同一直线(斜线)上,如果加上绝对值
,那么也满足其垂线
,整合起来就是在其所有斜线
方向上,即cols[i] - i == col - row
,等价的,进行移项得到cols[i] - col == i - row
,再移项得到分式(cols[i] - col) / i - row == 1
即直线方程的标准形式之一(y1 - y2) / (x1 - x2) = k
,说明两点在同一直线(斜线)上从第1行开始摆放皇后,每一行都有最多八种摆法,写一个在某一行开始摆放皇后的方法
place(int row)
在每一行上,遍历所有列,检查哪个位置可以摆放皇后,按照国际象棋规则检查不能摆的位置,即进行剪枝操作
剪枝的依据是,当前行之前的所有行的皇后摆法
不符合规则:
剪枝
符合规则:立刻摆放皇后,然后跳到下一行,即
place(row + 1)
,出现递归如果程序执行到了
place(row + 1)
的下一行,说明没有执行row + 2
那一行的摆放,即row + 1
行没法摆了,所以函数跑完了遍历一行的for循环,而place(row + 1)
的调用位置恰是place(row)
的代码中,可视为自动进行了回溯
如果开始摆放下标为8的行,说明全部的皇后都摆进去了,这就是
place
方法的递归退出条件,令结果集+1,然后退出递归
添加显示方法
写一个方法,来图形化的打印棋盘情况
观察回溯过程
在
isValid
方法中进行打印,观察回溯过程
代码优化——布尔数组
针对
isValid
进行优化
目前:对每个单独的格子,都要运行循环,对当前所有行进行检查
- 原因在于先前使用数组cols,用索引表示行,元素表示皇后所在的列
现在:用
3
个布尔类型的数组来存储
boolean[] cols
——标记某一列是否有皇后了boolean[] leftTop_righBottom
——标记某一对角线上是否有皇后了,左上角到右下角boolean[] rightTop_leftBottom
——标记某一对角线上是否有皇后了,右上角到左下角
isValid
方法
直接查询对应列有没有皇后
检查斜线,用两个布尔类型的数组来做,如下图所示,这两个数组的长度为
2n - 1
如何知道是哪一根对角线?
- 对于左上角到右下角:根据
row - col
的差值,以四皇后为例,row - col
的差值可以取-3 -2 -1 0 1 2 3
七种,为了使用数组,我们需要把这个区间平移到以0
开始,显然加上n - 1
就可以,对于四皇后,n == 4
,则采用row - col + 4 - 1
,得到差值0 1 2 3 4 5 6
- 用这一组差值作为数组的
索引
,按顺序表示对应的对角线,以左上到右下的对角线为例,索引为0
的就表示最靠右上角的对角线,索引为6
表示最靠左下角的对角线- 对于右上角到左下角:
索引
为row + col
,得到0 1 2 3 4 5 6
如果有效,就摆皇后,在这种情况下,
3
个数组都要摆如果无效剪枝,此时利用
continue
跳出本次循环即可如果发生回溯,对于布尔类型,需要重置上一个格子的值
问题
为什么之前的写法不用在回溯位置进行恢复现场?
- 因为之前用
cols[i]
标记第i
行的某列有皇后,回溯之后,cols[i]
的值会被新值覆盖
- 此处的写法使用布尔类型数组,标记了
整列
,整条对角线
,这个没办法覆盖,必须手动恢复状态
代码优化——位运算
可以利用位运算,进一步压缩空间复杂度
- 该方法只用于数据规模较小的情况,最好
n <= 8
- 以八皇后为例
- 不使用布尔数组,直接使用8位二进制数
byte
- 看对应二进制位的
0
和1
来表示先前布尔的false
和true
- 用8位二进制数
按位与
上1
左移列数
,如果等于0
就可以放;不等于0
就说明那位上有值,进行剪枝
- 放置皇后,就是将对应二进制位设置为
1
,采用按位或
可以做到- 回溯时,将对应为恢复,对应为和
0
与,其他为和1
与,为了方便实现,可以将1
先左移到达目标位置,然后把这个数取反,再和之前的数据进行与