八皇后问题其实是个在8*8的棋盘上摆放8个皇后的游戏,要求每个皇后都不同行,不同列,不在对角线上。然而涉及到棋之类的问题总是很复杂,想想alphago扬名立万的围棋就知道,下棋难着呢。尽管开篇我们就知道可以用位运算来解决啊,但是这就带来了另一座大山,啥是位运算?
题目
设计一种算法,打印 N 皇后在 N × N 棋盘上的各种摆法,其中每个皇后都不同行、不同列,也不在对角线上。这里的“对角线”指的是所有的对角线,不只是平分整个棋盘的那两条对角线。
注意:本题相对原题做了扩展
示例:
输入:4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释: 4 皇后问题存在如下两个不同的解法。
[
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/eight-queens-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
位运算篇
位运算就是直接对整数在计算机内存中的二进制位进行操作的运算,由于位运算直接对内存数据操作,无需转换,所以处理速度快。
其实位运算在我们工作和算法解题中并不常见,不过巧用位运算会提升你的level,还会提升性能,让代码可读性更好。
本文使用位运算解决八皇后问题学习自这篇文章
https://mp.weixin.qq.com/s/f2Xrgd1cnCJ8WfQXnbznSw。文章详述了位运算和解题思路,还提供了伪代码,不过由于leetcode的题目有所变动,所以我们最终的代码实现还是不太一样的。
回溯算法
所谓的使用位运算也只是存储的数据结构,我们解决这道八皇后题需要使用回溯算法。
回溯算法简单来说就是我们第二行的皇后怎么摆放取决于上一行,所以当某一行无法摆放皇后时,我们回到上一行重新调整,然后继续往下进行。回溯算法一般使用递归。
接下来的重点是位运算的回溯算法怎么处理?
找出当前行可放皇后的格子
首先,我们放皇后的顺序按照行进行,放完第一行,再放第二行,依次类推。这样我们能少考虑行这个条件。考虑是否可以放皇后,我们需要考虑前几行皇后所在的列column,左对角线pie(撇)和右对角线na(捺)三个条件。如果放了皇后,则当前格子对应的值为1,否则为0。
column | pie | na 得到的结果中值为 0 的代表当前行对应的格子可放皇后, 1 代表不能放。但是我们需要用1表示可放皇后,所以需要取反。c语言中取反为~(column| pie | na)。golang中为^(colomn | pie | na)。但是取反后高位的0全都变成1了,而我们只想保留低n位(其中n就是N皇后,八皇后的n=8)。这里最后的结论是
bits = ( ^(colomn | pie | na) ) & ( (1 << n) - 1 )
找出当前行如何选择皇后放在哪个格子
我们每次从当前行可用格子中取出最右边位为1的格子放置皇后。可以得出
p = bits & -bits
在运算过程中, -bits会以补码形式出现。所以刚好可以得到最右边位为1的值。
找出下一行的column,pie,na
这样我们就找到了当前行可放皇后的格子了,值为1的格子即是。由于回溯算法一般用递归,所以接下来我们需要循环遍历所有为1的格子,每次取出可用的放上皇后,再找下一层可放皇后的格子,依次递归,直到所有行遍历完毕结束。
那么我们需要直到下一行的限制条件如何演变。如果当前行选中p表示放了皇后的值,那么下一行的column,pie,na分别如下。
column=column|p
(pie|p)<<1
(na|p)>>1
Go代码
执行用时:4 ms
内存消耗:3.6 MB
var ans [][]string
func solveNQueens(n int) [][]string {
ans = nil
if n==1 {
return [][]string{{"Q"}}
}
if n<4 {
return nil
}
//存储位运算的整数
comb := make([]int,n)
//回溯
queenSettle(n,0,0,0,0,comb)
return ans
}
func queenSettle(n,row,colomn,pie,na int,comb []int) {
//放到最后一行,结束
if row == n {
var res []string
res = generateResult(comb)//整数转成对应二进制的字符串
ans = append(ans,res)
return
}
//获取当前行可放皇后的格子,对应格子位为1
bits := ( ( ^(colomn | pie | na) ) & ( (1 << n) - 1 ))
for ;bits>0; {
//取可放皇后的最右一格放皇后
p := bits & -bits
//下一行的bits需要去掉当前行皇后这一列
bits -= p
//保存位运算的整数
comb[row] = p
//进入下个递归
queenSettle(n,row+1,colomn|p,(pie|p)<<1,(na|p)>>1,comb)
}
return
}
//将位运算整数转成对应二进制的字符串,位1对应Q,位0对应.
func generateResult(comb []int) []string{
var res []string
for i:=0;i<len(comb);i++ {
str1 := make([]byte , len(comb))
for j:=0;j<len(comb);j++ {
if 1<<j == comb[i] {
str1[j] = 'Q'
} else {
str1[j] = '.'
}
}
s := string(str1)
res = append(res,s)
}
return res
}
最后让我们围观一下题解区时间和空间都打败100%对手的答案,也是使用了位运算的思想。
执行用时:0 ms
内存消耗:3.2 MB
func solveNQueens(n int) [][]string {
if n == 0 {
return nil
}
col := make([]bool, n)
dig1, dig2 := make([]bool, n<<1-1), make([]bool, n<<1-1)
var res [][]string
var tmp [][]byte
tmp = make([][]byte, n)
for i := range tmp {
tmp[i] = make([]byte, n)
for j := range tmp[i] {
tmp[i][j] = '.'
}
}
var dfs func(h int)
dfs = func(h int) {
if h == n {
t := make([]string, n)
for i := range t {
t[i] = string(tmp[i])
}
res = append(res, t)
} else {
for i := 0; i < n; i++ {
if !col[i] && !dig1[i-h+n-1] && !dig2[i+h] {
col[i], dig1[i-h+n-1], dig2[i+h] = true, true, true
tmp[h][i] = 'Q'
dfs(h+1)
tmp[h][i] = '.'
col[i], dig1[i-h+n-1], dig2[i+h] = false, false, false
}
}
}
}
dfs(0)
return res
}
总结
- 学练结合才能提升自己的实力,所以尽管花了一天的时间,我还是觉得很值得
- 发现leetcode验证代码正确的方法是将多个测试用例依次调用默认给的方法。所以需要保证幂等性。这里就涉及到我踩的坑,点击执行代码,答案正确,点击提交,总是显示在第2个测试用例的时候报错。通过打印发现,由于我设置了全局变量ans,所以第2个测试用例的结果中还带着第1个测试用例的结果。解决办法是在执行方法solveNQueens中多加一个清空ans的操作。
- Keep doing the hard and right thing.