这个花了我一天的位运算的王冠「八皇后问题」是个什么鬼?

八皇后问题其实是个在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
}

总结

  1. 学练结合才能提升自己的实力,所以尽管花了一天的时间,我还是觉得很值得
  2. 发现leetcode验证代码正确的方法是将多个测试用例依次调用默认给的方法。所以需要保证幂等性。这里就涉及到我踩的坑,点击执行代码,答案正确,点击提交,总是显示在第2个测试用例的时候报错。通过打印发现,由于我设置了全局变量ans,所以第2个测试用例的结果中还带着第1个测试用例的结果。解决办法是在执行方法solveNQueens中多加一个清空ans的操作。
  3. Keep doing the hard and right thing.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值