51. N-Queens

经典的 N 皇后问题,finally。

尝试优化,进行剪枝减少每次需要搜索的格子,比如正解中的后与后之间一定是一个 “马步”,因此只考虑在一个后被放置之后其他的“马步”位置。 然而这样会漏掉一些解,因为一个正解中不是所有后之间都是 “马步” 联系起来的

因此不能单凭“马步”去保证下一个正确的皇后位置,还是要老老实实遍历搜索。

一个比较简单直接的办法是,维护一个 list 代表当前的皇后位置(每一个位置都是valid)在考虑增加新一个王后时,和 list 里已有的每一个皇后比较是否 valid. 问题在于这个 list 的大小是和我们的棋盘大小 n 成正比的,假设一个 list 从0个皇后一直增加到n个皇后,在这个过程中要进行 1 + 2 + 3 ... + n = O(n^2) 次 isValid 操作。

  • 45度对角线的 row + column 值是恒等的(一加一减),并且每条对角线的 row + column 值唯一

  • 135度对角线的 row - column 值是恒等的(同加同减),并且每条对角线的 row - column 值唯一

  • 边长为 n 的棋盘,有 2n - 1 条对角线,并且中间值 (0,0) 的 index 为 n - 1

我们需要需要三个辅助的方法,第一个是DFS的主体程序,第二个是判断是否合法的攻击检测(皇后间是否冲突,第三个是将棋盘画成一个2D matrix返回。

两条对角线判定我推了好久,因为index从0开始和从1开始非常头痛。

  • rowIndex + cols.get(rowIndex) == row + column 45度对角线

  • rowIndex - cols.get(rowIndex) == row - column 135度对角线

public class Solution {
    public List<List<String>> solveNQueens(int n) {
        List<List<String>> result = new ArrayList<>();
        if (n == 0) return result;

        List<Integer> list = new ArrayList<>();
        dfs(result, list, n);
        return result;
    }
    private void dfs(List<List<String>> result, List<Integer> cols, int n){
        if (cols.size() == n) {
            List<String> str = drawChess(cols);
            result.add(str);
        }
        for (int colIndex = 0; colIndex < n; ++colIndex) {
            if (!isValid(cols, colIndex)) continue;
            cols.add(colIndex);
            dfs(result, cols, n);
            cols.remove(cols.size() - 1);
        }
    }
    private boolean isValid(List<Integer> cols, int colIndex){
        int row = cols.size();
        for (int rowIndex = 0; rowIndex < row; ++rowIndex) {
            if (cols.get(rowIndex) == colIndex) return false;
            if (rowIndex + cols.get(rowIndex) == row + colIndex) return false;
            if (rowIndex - cols.get(rowIndex) == row - colIndex) return false;
        }
        return true;
    }
    private List<String> drawChess(List<Integer> list){
        List<String> result = new ArrayList<>();
        if (list == null || list.size() == 0) return result;

        for (int i = 0; i < list.size(); ++i) {
            StringBuilder sb = new StringBuilder();
            for (int j = 0; j < list.size(); ++j) {
                if (j == list.get(i)) sb.append("Q");
                else sb.append(".");
            }
            result.add(sb.toString());
        }
        return result;
    }
}

在每步遍历搜索中, 都要 call 一次 isValid() 函数来判断当前格子是否有效,在 dfs + backtracking 的结构不变的情况下,isValid()函数的性能就会成为程序的瓶颈。

一个更取巧的方式是,利用一个皇后会占用这个格子的所有行,列以及对角线的特点,维护所有“行,列,45度对角线,135度对角线” 的 boolean array,这样在每次考虑一个新位置的时候,只需要 O(1) 的时间进行一次操作就可以了。

boolean[] 完全可以,Java 的 BitSet 也不错,不过我实际跑的结果来看,BitSet 在用时上会慢一些,原因在这个帖子:

Boolean-vs-bitset-which-is-more-efficient

boolean[] 一个 boolean 占用大约 1 byte 空间(JVM dependent),BitSet 平均占用 1 bit. boolean[] 的优点是更 CPU efficient, 只有在数组非常大(1000w以上)二者的速度持平。BitSet 内部实现是建立 long[] ,用 64-bit 的块去分配 BitSet 的字节。

  • 45度对角线的 row + column 值是恒等的(一加一减),并且每条对角线的 row + column 值唯一, 在 [0,1,2 ... 2n - 2] 区间

  • 135度对角线的 row - column 值是恒等的(同加同减),并且每条对角线的 row - column 值唯一,在[-(n-1) ... -2,-1,0, 1, 2 ... n - 1 ] 区间

  • 边长为 n 的棋盘,有 2n - 1 条对角线,并且中间值 (0,0) 的 index 为 n - 1

public class Solution {
    public List<List<String>> solveNQueens(int n) {
        boolean[] 
            //ocp0 = new boolean[n], 
            ocp90 = new boolean[n], 
            // 总共 2n - 1 种可能性,(0,0) 的 index 是 n - 1
            ocp45 = new boolean[2 * n - 1], 
            ocp135 = new boolean[2 * n - 1]; 
        List<List<String>> ans = new ArrayList<List<String>>();
        char[][] map = new char[n][n];
        for (char[] tmp : map) Arrays.fill(tmp, '.'); //init

        solve(0, n, map, ans, ocp45, ocp90, ocp135);
        return ans;
    }

    private void solve(int depth, int n, char[][] map, List<List<String>> ans, 
    boolean[] ocp45, boolean[] ocp90, boolean[] ocp135) {
        if (depth == n) {
            addSolution(ans, map);
            return;
        }

        for (int j = 0; j < n; j++)
        // 每次都进行新的一行,所以行不用检查
        // 90度代表棋盘的列
        // 45度对角线(从左上到右下)同一条线上 row + col 是相等的,因此可用作 index
        // 135度对角线(从左下到右上)可从 (0,0) 即 index (n - 1) 为起点
        // 因为同一条对角线 row - col 值恒定,可用作 offset 表示对角线的 index. 
            if (!ocp90[j] && !ocp45[depth + j] && !ocp135[j - depth + n - 1]) {
                ocp90[j] = true;
                ocp45[depth + j] = true;
                ocp135[j - depth + n - 1] = true;
                map[depth][j] = 'Q';

                solve(depth + 1, n, map, ans, ocp45, ocp90, ocp135);

                ocp90[j] = false;
                ocp45[depth + j] = false;
                ocp135[j - depth + n - 1] = false;
                map[depth][j] = '.';
            }
    }

    private void addSolution(List<List<String>> ans, char[][] map) {
        List<String> cur = new ArrayList<String>();
        for (char[] i : map) cur.add(String.valueOf(i));
        ans.add(cur);
    }
}

52. N-Queens II

很可惜,这题解与解之间不存在 optimal substructure 可以有效利用记忆化搜索。老老实实的 DFS + backtracking 还是必要的。

有一些比较妖孽的解法比如 Kunth's 的 Dancing Links,刷题面试的时候,这种大招还是没必要去开了。

既然不需要输出最终解,也就省去了传递 rst 和把棋盘转成 String 的过程。一个问题是,如何在这种dfs + backtracking中维护一个 primitive type 变量,比如有效解的总数?

Java 中默认 primitive type 是 pass by value,而且 Integer type 虽然作为一个 object 存在但是是 immutable, 不能用来作为全局参数多个函数共同更新。

我一直都不喜欢全局变量,感觉用起来太投机取巧了也不好看。

只求解的数量时,可以让 dfs 函数直接返回 int,每次迭代的时候把其他迭代的返回结果加起来就好了。

public class Solution {
    public int totalNQueens(int n) {
        if (n < 2) return n;
        List<Integer> list = new ArrayList<>();

        return dfs(list, n);
    }
    private int dfs(List<Integer> cols, int n) {
        if (cols.size() == n) return 1;

        int count = 0;
        for (int colIndex = 0; colIndex < n; ++colIndex) {
            if (!isValid(cols, colIndex)) continue;
            cols.add(colIndex);
            count += dfs(cols, n);
            cols.remove(cols.size() - 1);
        }
        return count;
    }
    private boolean isValid(List<Integer> cols, int colIndex) {
        int row = cols.size();
        for (int rowIndex = 0; rowIndex < row; ++rowIndex) {
            if (cols.get(rowIndex) == colIndex) return false;
            if (rowIndex + cols.get(rowIndex) == row + colIndex) return false;
            if (rowIndex - cols.get(rowIndex) == row - colIndex) return false;
        }
        return true;
    }
}

results matching ""

    No results matching ""