LeetCode 刷题记录(三)

时间:2022-07-23
本文章向大家介绍LeetCode 刷题记录(三),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

本篇文章主要介绍一个经典问题:「N 皇后」问题。

51. N-Queens

题目

下图为 8 皇后问题的一种解法。

「示例」

Input: 4
Output: [
 [".Q..",  // Solution 1
  "...Q",
  "Q...",
  "..Q."],

 ["..Q.",  // Solution 2
  "Q...",
  "...Q",
  ".Q.."]
]
Explanation: There exist two distinct solutions to the 4-queens puzzle as shown above.

思路

这道题是「回溯法」的经典应用。基本的思路是:从第一行开始,每行按列循环放置皇后,如果找到一个符合规则的位置,则到下一行,以此类推,如果可以一直进行到最后一行,则得到一个正确的解法,记录下来;如果到某一行发现没有符合要求的位置,就回到上一行,对该行还未循环的位置继续按列循环。重复上述过程,直到所有格子均被遍历。可以看出,这种解法实际上是一种「深度优先搜索」

在实际编程时,我们通过「递归」实现上述思路。注意不论一条路线是否最终得到可行解,由于每一行只能有一个皇后,因此我们都需要撤销当前的棋子摆放,以便进行下一条路线的尝试,二者的区别在于得到可行解的路线会一直递归到最底层,将解记录下来再进行回溯,而无可行解的路线一般在递归中途即回溯。

关于对角线的攻击路线,可以发现对于一个给定的棋子位置,其主对角线和次对角线的棋子的行列信息存在如下规律:主对角线(左上到右下)上 行号 - 列号 = 常数,次对角线上 行号 + 列号 = 常数。为了防止出现负数,我们会对主对角线对应的数组下标进行扩大。

代码

Java 解法

class Solution {
    int n; // 默认权限(仅同一包下的类可以访问)
    int rows[]; // 当前行的列位置记录
    int dales[]; // 主对角线位置记录
    int hills[]; // 次对角线位置记录
    int queens[]; // 皇后位置记录
    List<List<String>> output = new ArrayList<>();

    public List<List<String>> solveNQueens(int n) {
        // 初始化各数组
        this.n = n;
        rows = new int[n];
        dales = new int[2 * n - 1];
        hills = new int[2 * n - 1];
        queens = new int[n];

        backtrack(0); // 从第一行开始调用回溯法,遍历所有格子
        return output;

    }

    // 判断一个位置是否会被攻击到
    private boolean isNotUnderAttack(int row, int col) {
        int res = rows[col] + dales[row - col + n - 1] + hills[row + col];
        return (res == 0)? true : false;
    }

    // 放置皇后,设定可攻击的位置
    private void placeQueen(int row, int col) {
        queens[row] = col;
        rows[col] = 1; // 对应的列可攻击(回溯会确保每行只有一个,只需每行按列遍历)
        dales[row - col + n - 1] = 1; // 主对角线的对应位置可攻击,范围 [0, 2n-2]
        hills[row + col] = 1; // 次对角线的对应位置可攻击, 范围 [0, 2n-2]
    }

    // 取消当前位置的摆放,用于回溯
    private void removeQueen(int row, int col) {
        queens[row] = 0;
        rows[col] = 0;
        dales[row - col + n - 1] = 0;
        hills[row + col] = 0;
    }

    // 添加一个解
    private void addSoultion() {
        List<String> solution = new ArrayList<>(); // 加泛型写法更标准
        for (int i = 0; i < n; i++) { // 按行遍历
            int col = queens[i];
            StringBuilder sb = new StringBuilder();
            for (int j = 0; j < col; j++) sb.append(".");
            sb.append("Q");
            for (int j = 0; j < n - col - 1; j++) sb.append(".");
            solution.add(sb.toString());
        }
        output.add(solution);
    }

    // 回溯法核心(递归调用)
    private void backtrack(int row) {
        for (int col = 0; col < n; col++) {
            if (isNotUnderAttack(row, col)) {
                placeQueen(row, col);
                if (row == n - 1) addSoultion(); // 如果已到达最后一行,则得到一个解
                else backtrack(row + 1); // 否则继续向下一行前进,继续递归
                removeQueen(row, col); // 撤销当前位置,开始探索下一条路线
            }
        } // 如果当前行没有可放置的位置,则直接返回上一行开始探索下一条路线
    }
}

Python 解法

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        def could_place(row, col): # python 可以函数嵌套
            return not(rows[col] + dale_diagonais[row - col + n - 1] + hill_diagonais[row + col])
        
        def place_queen(row, col):
            queens.add((row, col))
            rows[col] = 1
            dale_diagonais[row - col + n - 1] = 1
            hill_diagonais[row + col] = 1
        
        def remove_queen(row, col):
            queens.remove((row, col))
            rows[col] = 0
            dale_diagonais[row - col + n - 1] = 0
            hill_diagonais[row + col] = 0
        
        def add_solution():
            solution = []
            for _, col in sorted(queens): # 先按行排序,再按列排序
                solution.append('.' * col + 'Q' + '.' * (n - col - 1))
            output.append(solution)
        
        def backtrack(row = 0): # 默认从 0 开始
            for col in range(n):
                if could_place(row, col):
                    place_queen(row, col)
                    if row + 1 == n:
                        add_solution()
                    else:
                        backtrack(row + 1)
                    remove_queen(row, col)

        rows = [0] * n # 快速创建 n 维数组(列表)
        dale_diagonais = [0] * (2 * n - 1) # list 做全局变量无需声明 global
        hill_diagonais = [0] * (2 * n - 1)
        queens = set() # 无重复的 tuple 集合
        output = []
        backtrack() # python 需要先定义再调用
        return output

52. N-Queens II

题目

给定一个整数n ,返回 n皇后不同的解决方案的数量。

「示例」

Input: 4
Output: 2
Explanation: There are two distinct solutions to the 4-queens puzzle as shown below.
[
 [".Q..",  // Solution 1
  "...Q",
  "Q...",
  "..Q."],

 ["..Q.",  // Solution 2
  "Q...",
  "...Q",
  ".Q.."]
]

思路

这道题跟上一题思路相同,只是输出简单了一些。这里给出一个使用「位运算」的精妙解法。首先简单介绍一下本解法中使用到的与位运算相关的概念及性质。在位运算中,正负数的运算是基于「补码」实现的。下图给出了一个八位二进制数的补码系统表示,最高位为符号位,正数和 0 的补码为其本身,负数的补码为其对应的正数取反加一。

基于补码,我们将使用如下的两个位运算操作:

  • x & -x「按位与」一个数与其负数,这里负数会被表示成补码,可以验证,该操作会将原数字的最后一位出现的 1 保留,其它位数全部清 0(实际运算时不需要考虑符号位,一般不会超出位数限制,就算考虑也会变成 0)
  • x & (x - 1)「按位与」一个数和其自身减 1,可以验证,该操作会将原数字的最后一位出现的 1 变成 0,其它位数保持不变。上述操作也可以通过「异或」来实现:x ^ (x & -x),因为 0 与任意数(0 或 1)异或的结果为原数字,所以只有最后一位出现的 1 会被清 0。

「举例说明」

127 & -127:
(01111111 & 10000001) = 00000001 // 只有最后一个 1 被保留
  
127 & 126
(01111111 & 01111110) = 01111110 // 只有最后一个 1 被清 0

2 & 1
(00000010 & 00000001) = 00000000 // 即使存在借位上述结论也成立

基于上述位运算,我们可以给出一种巧妙的解法。算法的整体思路和之前相同,使用基于回溯的深度优先搜索。区别在于我们会使用 3 个「二进制数」来分别标记当前行哪些格子可以放置皇后,它们分别表示:列、主对角线和次对角线,二进制为 1 代表不能放置,二进制为 0 代表可以放置。注意在实际编码时,对于当前行所有可用的位置,我们会用 1 表示可以放置,0 表示不能放置,这是位运算的特性导致的,因此我们在合并时需要对一方进行「取反」操作。

代码

Java 解法

class Solution {
    int count = 0; // 初始化最终输出
    public int totalNQueens(int n) {
        backtrack(n, 0, 0, 0, 0); // 从第一行开始执行搜索
        return count;
    }

    private void backtrack(int n, int row, int col, int dale, int hill) {
        /**
         row: 当前行号
         col: 当前行按列筛选的的位置 [1 = 不可放置,0 = 可以放置]
         dale: 当前行按主对角线筛选的位置 [1 = 不可放置,0 = 可以放置]
         hill: 当前行按次对角线筛选的位置 [1 = 不可放置,0 = 可以放置]
         */
        if (row >= n) count++; // 如果已经遍历完所有行,则得到一个解(这里是到达最后一行的下一行)
        else { // 否则继续搜索
            // 通过位运算得到当前行所有可放置的位置 [1 = 可以放置,0 = 不可放置]
            int bits = ((1 << n) - 1) & ~(col | dale | hill); // 第一项得到 n 个 1,表示均可放置
            while (bits > 0) { // 如果存在可以放置的位置
                int pick = bits & -bits; // 通过位运算筛选出一个 1,表示当前放置的位置
                // 递归调用,向下一行搜索,按位或操作可以将当前放置位置所产生的攻击位置加入进去
                backtrack(n, row + 1, (col | pick), (dale | pick) >> 1, (hill | pick) << 1);
                bits &= bits - 1; // 通过位运算实现回溯,将当前放置的位置置为 0(不可放置)
            }
        }
    }
}

Python 解法

class Solution:
    def totalNQueens(self, n: int) -> int:
        def backtrack(row = 0, col = 0, dale = 0, hill = 0, count = 0):
            if row == n:
                # 注意 count 定义为全局变量的话需要声明,不然会报错(赋值时存在歧义,被理解为局部变量)
                # 类中 global 不管用,得使用 self 表示全局变量(global 需要引用类外面的变量)
                count += 1
            else:
                bits = ((1 << n) - 1) & ~(col | dale | hill)
                while bits:
                    pick = bits & -bits
                    count = backtrack(row + 1, (col | pick), (dale | pick) >> 1, (hill | pick) << 1, count)
                    bits &= bits - 1   
            return count   
        return backtrack()