N 皇后问题

N 皇后

【题目】

N 皇后问题是指在 N*N 的棋盘上要摆 N 个皇后,要求任何两个皇后不同行、不同列,也不在同一条斜线上。给定一个整数 n,返回 n 皇后的摆法有多少种。

例子

n=1,返回 1

n=2或3,返回 0

n=8,返回 92

【分析】

以行为单位进行判断,这样做的好处是,不需要考虑皇后共行的问题,只需要考虑共列或者共斜线的问题。

上面在第一行 (0, 0)的位置放置了第一个皇后之后,开始判断第二行的位置

依次判断 (1, 0)(共列) 、(1, 1)(共斜线)

等到了 (1, 2),找到了第二个皇后的位置,找到之后,进行递归调用,找第三个皇后的位置

依次判断第三行的各个位置,发现都没有第三个皇后的立足之地,当前递归函数结束,返回到上一层调用处,即 (1, 2) 的位置,这说明当前的决策有问题,第二个皇后需要调整位置,找到了 (1, 3) 的位置,修改相关变量:

基于当前的决策,继续向下进行递归调用,即判断第三行皇后的位置。和上面的流程一样,找到了 (2, 1) 的位置

继续往下层递归调用,寻找第四个皇后的位置。通过一轮验证,发现第四行四个位置都没有皇后的合适位置,当前递归函数结束,返回到上一层,即 (2, 1) 的位置,从 (2, 2) 继续往后验证。

通过一轮验证,发现第三行剩余位置都没有皇后的合适位置,则说明当前决策有问题,当前递归过程结束,返回到上一层递归函数,即 (1, 3) 的位置,由于第二行已经是验证完毕,没有位置了,说明第二行的决策也有问题,返回到上一层递归函数,即 (0, 0) 的位置。

第一个皇后从 (0, 1) 的位置决策,重复上面描述的步骤。

代码如下:

public class Code19_NQueens {

    public static void main(String[] args) {
        int n = 4;
        System.out.println(num1(4));
    }

    public static int num1(int n) {
        // base case
        if (n < 1) {
            return 0;
        }
        // 记录皇后的位置,数组下标表示行,值表示列
        int[] record = new int[n];
        return process1(0, record, n);
    }

    /**
     * @param i      来到了第 i 行
     * @param record recode[0..i-1] 表示之前的行,放了的皇后的位置,即已经决策好的
     * @param n      代表整体一共有多少行 0~n-1
     * @return 摆完所有的皇后,合理的摆法有多少种
     */
    public static int process1(int i, int[] record, int n) {
        if (i == n) {
            return 1;
        }
        int res = 0;
        // j 代表列
        for (int j = 0; j < n; j++) {
            // 当前 i 行的皇后,放在 j 列,判断是否有效,即和之前 (0..i-1) 的决策是否共行、共列、共斜线
            if (isValid(record, i, j)) {
                record[i] = j;
                // 有效的话,递归验证下一行
                res += process1(i + 1, record, n);
            }
        }
        return res;
    }

    /**
     * 验证 (i, j) 位置是否和之前的决策共列或共斜线
     */
    private static boolean isValid(int[] record, int i, int j) {
        for (int k = 0; k < i; k++) {
            // 共列 or 共斜线 判定
            if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
                return false;
            }
        }
        return true;
    }

}

在此基础上改造了 leetcode 51

class Solution {
    public List<List<String>> solveNQueens(int n) {
        if (n < 1) {
            return new ArrayList<>();
        }
        List<List<String>> res = new ArrayList<>();
        // 记录皇后的位置,数组下表表示行,值表示列
        int[] record = new int[n];
        process1(0, record, n, res);
        return res;
    }

    /**
     * @param i      来到了第 i 行
     * @param record recode[0..i-1] 表示之前的行,放了的皇后的位置,即已经决策好的
     * @param n      代表整体一共有多少行 0~n-1
     * @return
     */
    public void process1(int i, int[] record, int n, List<List<String>> res) {
        if (i == n) {
            res.add(new ArrayList<String>(assemblePos(record)));
        }
        // j 代表列
        for (int j = 0; j < n; j++) {
            // 当前 i 行的皇后,放在 j 列,判断是否有效,即和之前 (0..i-1) 的决策是否共行、共列、共斜线
            if (isValid(record, i, j)) {
                record[i] = j;
                // 有效的话,递归验证下一行
                process1(i + 1, record, n, res);
            }
        }
    }

    private List<String> assemblePos(int[] record) {
        char[][] temp = new char[record.length][record.length];
        for (int i = 0; i < temp.length; i++) {
            for (int j = 0; j < temp[0].length; j++) {
                temp[i][j] = '.';
            }
        }
        for (int i = 0; i < record.length; i++) {
            temp[i][record[i]] = 'Q';
        }
        List<String> ans = new ArrayList<>();
        for (char[] chars : temp) {
            ans.add(new String(chars));
        }
        return ans;
    }

    /**
     * 验证 (i, j) 位置是否和之前的决策共列或共斜线
     */
    private boolean isValid(int[] record, int i, int j) {
        for (int k = 0; k < i; k++) {
            // 共列 or 共斜线 判定
            if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
                return false;
            }
        }
        return true;
    }
}

代码优化

使用位运算进行代码的优化

需要准备三个变量,作为位运算的容器,比如 Java 中 int 可以用 32 bit 来表示,1 表示这个位置不可以放置皇后,0 表示这个位置可以放置皇后。

三个变量 colLimitleftDiaLimrightDiaLim 分别限制列、左斜线、右斜线在 N 皇后的期盼上面有没有放置限制。

举个例子:

假设我们在 N*N 的棋盘的第一行,(0, 4) 的位置,放置了一个皇后。则 colLimit 这个相应的 bit 位置应该设置为 1,表示这个列已经不可以再放置皇后了。

再考虑 leftDiaLimrightDiaLim 该如何处理呢?

对于第二行,应该是 (1, 3) 和 (1, 5) 两个位置不能放置皇后,因为其和 (0, 4) 位置在同一条对角线上面。所以 leftDiaLimcolLimit 的基础上左移一位即可,rightDiaLimcolLimit 的基础上右移一位即可。如下图:

当判断第二行的哪些位置可以放置皇后的时候,只需要将上面的三个变量进行 | 运算即可得到哪些位置可以尝试放置皇后。

假设,第二个皇后放置到了 (1, 1) 的位置,则对于第三行来说,不可放置的位置如下图所示:

![image-20220428143935862](G:\01-左神-算法与数据结构基础班【完结】\NQueens\01. N皇后.assets\image-20220428143935862.png)

对于 colLimit 变量好处理,即在相应的位置标记为 1 即可。

而对于 leftDiaLim 来说,第一行 (0, 4) 的皇后对第三行的左对角线上的影响,相对于之前是向左移动了一位,第二行皇后 (1, 1) 对第三行的影响还是左下左移一位,所以总体来说 leftDiaLim 需要左移一位。rightDiaLim 同理需要右移一位。最后的数据如下图:

当判断第三行的哪些位置可以放置皇后的时候,只需要将上面的三个变量进行 | 运算即可得到哪些位置可以尝试放置皇后。

……

以后的步骤都是如此,,,

代码如下:

/**
 * 使用位运算优化
 */
public static int num2(int n) {
    if (n < 1 || n > 32) {
        return 0;
    }
    // 如果是 n 皇后问题,limit 最右是 n 个 1,其他都是 0(32 个 1 的话,值是 -1)
    int limit = n == 32 ? -1 : (1 << n) - 1;
    return process2(limit, 0, 0, 0);
}

/**
 * @param limit       划定问题的规模,几皇后问题?固定变量
 * @param colLimit    列的限制,1 的位置表示不能放皇后,0 的位置表示可以放皇后
 * @param leftDiaLim  左斜线的限制
 * @param rightDiaLim 右斜线的限制
 * @return
 */
private static int process2(int limit, int colLimit, int leftDiaLim, int rightDiaLim) {
    // corner case
    if (colLimit == limit) {
        return 1;
    }
    // 总的限制(解决左侧超出问题规模的 1 的干扰),结果是每一个 1 的位置是可以放皇后的
    int pos = limit & (~(colLimit | leftDiaLim | rightDiaLim));
    int mostRightOne = 0;
    int res = 0;
    // 尝试 post 中所有的 1
    while (pos != 0) {
        // 提出 post 中,最右侧的 1,即放皇后的位置
        mostRightOne = pos & (~pos + 1);
        pos = pos - mostRightOne;
        res += process2(limit, colLimit | mostRightOne, (leftDiaLim | mostRightOne) << 1, (rightDiaLim | mostRightOne) >>> 1);
    }
    return res;
}

上面代码中有几个点需要补充说明下:

代码中有 limit 变量是用来控制问题的规模,也是利用的二进制位表示,比如 8 皇后问题,最右是 8 个 bit 全是 1,左边全是 0,9 皇后问题,右边 9 个 bit 全是 1,左边全是 0,……,如果达到 32 bit 全是 1,则对应的只就是 -1。

int pos = limit & (~(colLimit | leftDiaLim | rightDiaLim)); 这行代码,是处理二进制位左侧对于程序的影响,又利用了 limit 控制里问题的规模。最后计算出来的二进制,只需要尝试位置为 1 的 bit 位即可。

尝试所有 bit 位为 1 的逻辑是:

int mostRightOne = 0;
// 尝试 post 中所有的 1
while (pos != 0) {
    // 提出 post 中,最右侧的 1,即放皇后的位置
    mostRightOne = pos & (~pos + 1);
    pos = pos - mostRightOne;
    ……
}

递归的执行上面的流程。

【时间复杂度】
由于每一行都要尝试 N 次,一共 N 行,所以时间复杂度是 O(N*N)

posted @ 2022-04-27 15:43  liangdao  阅读(163)  评论(0编辑  收藏  举报