位运算的应用

LeetCode37解数独

回溯

利用boolean 数组进行标识。当然用hashmap也可以。

class Solution {
    private boolean[][] line = new boolean[9][9];
    private boolean[][] column = new boolean[9][9];
    private boolean[][][] block = new boolean[3][3][9];
    private boolean valid = false;
    private List<int[]> spaces = new ArrayList<int[]>();

    public void solveSudoku(char[][] board) {
        for (int i = 0; i < 9; ++i) {
            for (int j = 0; j < 9; ++j) {
                if (board[i][j] == '.') {
                    spaces.add(new int[]{i, j});
                } else {
                    int digit = board[i][j] - '0' - 1;
                    line[i][digit] = column[j][digit] = block[i / 3][j / 3][digit] = true;
                }
            }
        }

        dfs(board, 0);
    }

    public void dfs(char[][] board, int pos) {
        if (pos == spaces.size()) {
            valid = true;
            return;
        }

        int[] space = spaces.get(pos);
        int i = space[0], j = space[1];
        for (int digit = 0; digit < 9 && !valid; ++digit) {
            if (!line[i][digit] && !column[j][digit] && !block[i / 3][j / 3][digit]) {
                line[i][digit] = column[j][digit] = block[i / 3][j / 3][digit] = true;
                board[i][j] = (char) (digit + '0' + 1);
                dfs(board, pos + 1);
                line[i][digit] = column[j][digit] = block[i / 3][j / 3][digit] = false;
            }
        }
    }
}

位运算

思路与算法

在方法一中,我们使用了长度为 9 的数组表示每个数字是否出现过。我们同样也可以借助位运算,仅使用一个整数表示每个数字是否出现过。

具体地,数 b 的二进制表示的第 i 位(从低到高,最低位为第 0 位)为 1,当且仅当数字 i+1 已经出现过。例如当 b 的二进制表示为 (011000100)_2时,就表示数字 3,7,8 已经出现过。

位运算有一些基础的使用技巧。下面列举了所有在代码中使用到的技巧:具体参考以下文章
https://leetcode-cn.com/problems/sudoku-solver/solution/jie-shu-du-by-leetcode-solution/

class Solution {
    private int[] line = new int[9];
    private int[] column = new int[9];
    private int[][] block = new int[3][3];
    private boolean valid = false;
    private List<int[]> spaces = new ArrayList<int[]>();

    public void solveSudoku(char[][] board) {
        for (int i = 0; i < 9; ++i) {
            for (int j = 0; j < 9; ++j) {
                if (board[i][j] == '.') {
                    spaces.add(new int[]{i, j});
                } else {
                    int digit = board[i][j] - '0' - 1;
                    flip(i, j, digit);
                }
            }
        }

        dfs(board, 0);
    }

    public void dfs(char[][] board, int pos) {
        if (pos == spaces.size()) {
            valid = true;
            return;
        }

        int[] space = spaces.get(pos);
        int i = space[0], j = space[1];
        int mask = ~(line[i] | column[j] | block[i / 3][j / 3]) & 0x1ff;
        for (; mask != 0 && !valid; mask &= (mask - 1)) {
            int digitMask = mask & (-mask);
            int digit = Integer.bitCount(digitMask - 1);
            flip(i, j, digit);
            board[i][j] = (char) (digit + '0' + 1);
            dfs(board, pos + 1);
            flip(i, j, digit);
        }
    }

    public void flip(int i, int j, int digit) {
        line[i] ^= (1 << digit);
        column[j] ^= (1 << digit);
        block[i / 3][j / 3] ^= (1 << digit);
    }
}
转载:
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/sudoku-solver/solution/jie-shu-du-by-leetcode-solution/

枚举优化

如果一个空白格只有唯一的数可以填入,也就是其对应的 b 值和 b-1 进行按位与运算后得到 0(即 b 中只有一个二进制位为 1)。此时,我们就可以确定这个空白格填入的数,而不用等到递归时再去处理它。

这样一来,我们可以不断地对整个数独进行遍历,将可以唯一确定的空白格全部填入对应的数。随后我们再使用与方法二相同的方法对剩余无法唯一确定的空白格进行递归 + 回溯

class Solution {
    private int[] line = new int[9];
    private int[] column = new int[9];
    private int[][] block = new int[3][3];
    private boolean valid = false;
    private List<int[]> spaces = new ArrayList<int[]>();

    public void solveSudoku(char[][] board) {
        for (int i = 0; i < 9; ++i) {
            for (int j = 0; j < 9; ++j) {
                if (board[i][j] != '.') {
                    int digit = board[i][j] - '0' - 1;
                    flip(i, j, digit);
                }
            }
        }

        while (true) {
            boolean modified = false;
            for (int i = 0; i < 9; ++i) {
                for (int j = 0; j < 9; ++j) {
                    if (board[i][j] == '.') {
                        int mask = ~(line[i] | column[j] | block[i / 3][j / 3]) & 0x1ff;
                        if ((mask & (mask - 1)) == 0) {
                            int digit = Integer.bitCount(mask - 1);
                            flip(i, j, digit);
                            board[i][j] = (char) (digit + '0' + 1);
                            modified = true;
                        }
                    }
                }
            }
            if (!modified) {
                break;
            }
        }

        for (int i = 0; i < 9; ++i) {
            for (int j = 0; j < 9; ++j) {
                if (board[i][j] == '.') {
                    spaces.add(new int[]{i, j});
                }
            }
        }

        dfs(board, 0);
    }

    public void dfs(char[][] board, int pos) {
        if (pos == spaces.size()) {
            valid = true;
            return;
        }

        int[] space = spaces.get(pos);
        int i = space[0], j = space[1];
        int mask = ~(line[i] | column[j] | block[i / 3][j / 3]) & 0x1ff;
        for (; mask != 0 && !valid; mask &= (mask - 1)) {
            int digitMask = mask & (-mask);
            int digit = Integer.bitCount(digitMask - 1);
            flip(i, j, digit);
            board[i][j] = (char) (digit + '0' + 1);
            dfs(board, pos + 1);
            flip(i, j, digit);
        }
    }

    public void flip(int i, int j, int digit) {
        line[i] ^= (1 << digit);
        column[j] ^= (1 << digit);
        block[i / 3][j / 3] ^= (1 << digit);
    }
}

补充:

按照行,列进行回溯。

    //回溯算法
    public boolean solveSudoku(char[][] board) {
        return backTrace(board, 0, 0);
    }

    //注意这里的参数,row表示第几行,col表示第几列。
    private boolean backTrace(char[][] board, int row, int col) {
        //注意row是从0开始的,当row等于board.length的时候表示数独的
        //最后一行全部读遍历完了,说明数独中的值是有效的,直接返回true
        if (row == board.length)
            return true;
        //如果当前行的最后一列也遍历完了,就从下一行的第一列开始。这里的遍历
        //顺序是从第1行的第1列一直到最后一列,然后第二行的第一列一直到最后
        //一列,然后第三行的……
        if (col == board.length)
            return backTrace(board, row + 1, 0);
        //如果当前位置已经有数字了,就不能再填了,直接到这一行的下一列
        if (board[row][col] != '.')
            return backTrace(board, row, col + 1);
        //如果上面条件都不满足,我们就从1到9种选择一个合适的数字填入到数独中
        for (char i = '1'; i <= '9'; i++) {
            //判断当前位置[row,col]是否可以放数字i,如果不能放再判断下
            //一个能不能放,直到找到能放的为止,如果从1-9都不能放,就会下面
            //直接return false
            if (!isValid(board, row, col, i))
                continue;
            //如果能放数字i,就把数字i放进去
            board[row][col] = i;
            //如果成功就直接返回,不需要再尝试了
            if (backTrace(board, row, col))
                return true;
            //否则就撤销重新选择
            board[row][col] = '.';
        }
        //如果当前位置[row,col]不能放任何数字,直接返回false
        return false;
    }

    //验证当前位置[row,col]是否可以存放字符c
    private static boolean isValid(char[][] board, int row, int col, char c) {
        for (int i = 0; i < 9; i++) {
            //当前列有没有和字符c重复的
            if (board[i][col] == c)
                return false;
            //当前行有没有和字符c重复的
            if (board[row][i] == c)
                return false;
            //当前的单元格内是否有和字符c重复的
            if (board[3 * (row / 3) + i / 3][3 * (col / 3) + i % 3] == c)
                return false;
        }
        return true;
    }
转载:
作者:sdwwld
链接:https://leetcode-cn.com/problems/sudoku-solver/solution/hui-su-jie-jue-by-sdwwld/

位运算加贪心

经常玩数独的可能知道,在填数字的时候一般都是选择那些可填数字比少的位置来填,所以这里也可以使用这种方式。这里为了便于计算,稍微修改了一点,每行每列每个单元格都使用一个int数字来记录,因为int类型是32位,而数独的行和列以及单元格大小都是9,所以使用int来存储足够了,这里只使用int类型的低9位来存储。举个简单的例子,比如第一行已经填了3个数字,比如示例中的5,3,7。那么我们就可以使用一个int类型的数字来表示

       //数独的长宽大小
        final int N = 9;
        //行
        private int[] rows = new int[N];
        //列
        private int[] cols = new int[N];
        //单元格
        private int[][] cells = new int[3][3];

        public void solveSudoku(char[][] board) {
            //统计未填的个数
            int count = 0;
            for (int i = 0; i < N; i++) {
                for (int j = 0; j < N; j++) {
                    char ch = board[i][j];
                    if (ch == '.') {
                        count++;
                    } else {
                        //如果已经有数字,把这个数字标记一下
                        fillNumber(i, j, ch - '1', true);
                    }
                }
            }
            //上面是一些计算前的准备工作,从这里开始调用回溯算法
            backtrace(board, count);
        }

        private boolean backtrace(char[][] board, int count) {
            //如果可填的位置为0,就是填完了,直接返回true
            if (count == 0) {
                return true;
            }
            //找到可选择数字比较少的位置
            int[] pos = getMinOkMaskCountPos(board);
            int x = pos[0], y = pos[1];
            //获取可选择数字比较少的位置的mask
            int mask = getMask(x, y);
            for (char c = '1'; c <= '9'; c++) {
                int index = c - '1';
                //判断这个位置是否可以填字符c
                if (testMask(mask, index)) {
                    //如果可填,就把字符c填入到这个位置中
                    fillNumber(x, y, index, true);
                    board[x][y] = c;
                    //如果成功直接返回
                    if (backtrace(board, count - 1))
                        return true;
                    //否则,撤销上面的操作
                    board[x][y] = '.';
                    fillNumber(x, y, index, false);
                }
            }
            return false;
        }

        //如果fill是true就把对应位置的数字从右边数第(n+1)位变为1,如果fill为false就把
        //对应位置的数字从右边数第(n+1)位变为0,
        private void fillNumber(int x, int y, int n, boolean fill) {
            if (fill) {
                int mask = 1 << n;
                rows[x] = rows[x] | mask;
                cols[y] = cols[y] | mask;
                cells[x / 3][y / 3] = cells[x / 3][y / 3] | mask;
            } else {
                int mask = ~(1 << n);
                rows[x] = rows[x] & mask;
                cols[y] = cols[y] & mask;
                cells[x / 3][y / 3] = cells[x / 3][y / 3] & mask;
            }
        }

        //当前位置的行,列,单元格进行与运算,运算的结果就是如果这个数字的
        //后面9位哪一个位置是0,就表示这个位置可以填对应的数字
        private int getMask(int x, int y) {
            return rows[x] | cols[y] | cells[x / 3][y / 3];
        }

        //统计上面的方法有多少位置还可以填
        private int getCount(int mask) {
            int count = 0;
            for (int i = 0; i < N; i++) {
                if ((mask & (1 << i)) == 0)
                    count++;
            }
            return count;
        }

        //判断mask从右边数第(index+1)个位置是否可以填入数字,
        //注意这里的index是从0开始,如果是0,就表示判断右边第1位
        //能不能填入数字
        private boolean testMask(int mask, int index) {
            return (mask & (1 << index)) == 0;
        }

        //统计所有的单元格,判断哪个单元格内可填数字比较少,就返回哪个单元格的坐标
        private int[] getMinOkMaskCountPos(char[][] board) {
            int[] res = new int[2];
            int min = 10;
            for (int i = 0; i < N; i++) {
                for (int j = 0; j < N; j++) {
                    if (board[i][j] == '.') {
                        int mask = getMask(i, j);
                        int count = getCount(mask);
                        if (count < min) {
                            min = count;
                            res[0] = i;
                            res[1] = j;
                        }
                    }
                }
            }
            return res;
        }
转载:
作者:sdwwld
链接:https://leetcode-cn.com/problems/sudoku-solver/solution/hui-su-jie-jue-by-sdwwld/

总结:位运算太强了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值