【算法刷题】解数独

时间:2020-09-16
本文章向大家介绍【算法刷题】解数独,主要包括【算法刷题】解数独使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

本文为个人解题思路整理,水平有限,有问题欢迎交流


概览

本题已数独问题为背景,要求计算出唯一解,表面是一个暴力深搜和回溯的问题,然而实际上如何优化才是精华所在

难度:中等

核心知识点:DFS(回溯)、状态压缩、位运算


题目来源

力扣:https://leetcode-cn.com/problems/sudoku-solver


题目内容

编写一个程序,通过已填充的空格来解决数独问题。

一个数独的解法遵循如下规则:

  • 数字 1-9 在每一行只能出现一次。
  • 数字 1-9 在每一列只能出现一次。
  • 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。

本题满足以下设定:

  • 给定的数独序列只包含数字 1-9 和字符 '.'
  • 你可以假设给定的数独只有唯一解。
  • 给定数独永远是 9x9 形式的。

样例

源数据

结果

解题思路

  • 基本思路:显然可以使用递归DFS暴力搜索每个可能的结果

    • 若最终所有结果被排除则搜索失败
    • 若所有空白被填充即搜索成功
  • 剪枝:只需要搜索空白点就可以了,那么可以将这些空白点整理出来

  • 剪枝:玩过数独的都知道先从选择少的点下手,这里同理,可以减少搜索的可能路径数量

  • 优化:搜索的时候需要确定当前点可能的数字,而这些数字要求不允许与同行、同列、同块(3*3)相同,那么可以提前将每行、每列、每块已出现的数字存储起来,只需要检查某个点所在的行列块就能知道可选数字,第一想法是存在数组里,

  • 优化:行列快均只允许1-9数字,那么可以用二进制数字表示,第i位为1则代表数字i出现过,为0则代表没出现过,注意二级制高位在右边,比如第一行的二进制是001010100,这样处理带来下面几个好处

    • 每行仅需要一个10位二进制数字表示即可,最大也就2047,总共只需要三个int[9]即可分别存放行、列、块的状态

    • 使用位运算进行数据变更或者检查都极其方便(性能消耗也小)

      如某个目标状态为states,进行以下操作

      • 将第i位设为1state |= 1 << i
      • 将第i位设为0state ^= 1 << i
      • 检查第i位是否为0:(state >> i) % 2 == 0
  • 优化:因为只需要获得唯一解,那么放置一个全部变量用于标记是否找到答案即可。当标记为true的时候即结束所有搜索,不再搜索;若检查了所有可能性,该标记仍未false则证明没有答案

    当然,如果答案不止一个就不能这么处理了


解题思路确定,开始整理解题方案


解题方案

  1. 遍历整个棋盘的每个点,以计算行、列、块的状态,并获取

    • 若该点为.,证明为空白,将这个点存储在列表中
    • 若该点不位.,证明为数字,将相关的行、列、块中标记已出现过这个数字
  2. 开始深度搜索

    1. 获取选择可能性最小的点position,并计算其可能的所有数字

    2. 检查position是否存在,不存在则证明所有空点已填充,修改标记为搜索成功,并结束递归

    3. 用数字i枚举1-9

      1. 检查是否是否允许数字i,不允许则跳过

      2. 修改数据

        1. 将当前搜索的点修改为数字i

        2. 修正与当前相关的行、列、块的状态

        3. 将当前点标记为已填充

      3. 进行下一层搜索

      4. 检查搜索成功的标记,若搜索成功则结束递归

      5. 撤回修改数据

        1. 将当前搜索的点修改为空白.

        2. 修正与当前相关的行、列、块的状态

        3. 将当前点标记为未填充


完整代码

class DemoBasicApplicationTests {
    @Test
    void test() {
        char[][] board = {
                {'5', '3', '.', '.', '7', '.', '.', '.', '.'},
                {'6', '.', '.', '1', '9', '5', '.', '.', '.'},
                {'.', '9', '8', '.', '.', '.', '.', '6', '.'},
                {'8', '.', '.', '.', '6', '.', '.', '.', '3'},
                {'4', '.', '.', '8', '.', '.', '.', '.', '1'},
                {'7', '.', '.', '.', '2', '.', '.', '.', '6'},
                {'.', '6', '.', '.', '.', '.', '2', '8', '.'},
                {'.', '.', '.', '4', '1', '9', '.', '.', '5'},
                {'.', '.', '.', '.', '8', '.', '.', '7', '9'}
        };
        solveSudoku(board);
    }

    public int[] col = new int[9];//行
    public int[] row = new int[9];//列
    public int[][] block = new int[3][3];//块
    List<Integer> list = new ArrayList<>();//空白位列表
    boolean flag = false;//标记,用于识别搜索是否成功

    /**
     * 解决方案
     */
    public void solveSudoku(char[][] board) {
        //初始化
        init(board);
        //执行dfs搜索
        dfs(board);
        //打印结果
//        System.out.println(flag);
        out(board);
    }

    /**
     * 打印board
     * 调试用
     *
     * @param board 目标board
     */
    public void out(char[][] board) {
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                System.out.print(board[i][j] + "    ");
            }
            System.out.println();
        }
        System.out.println();
    }

    /**
     * 初始化,填充行、列、块以及空白位列表的值
     *
     * @param board 目标board
     */
    public void init(char[][] board) {
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                if (board[i][j] != '.') {
                    //不为空的时候,更新行、列、块的值
                    update(i, j, board);
                } else {
                    //为空,则将其添加到空白位列表
                    list.add(i * 9 + j);
                }
            }
        }
    }

    /**
     * 执行搜索
     * 在排除所有可能后结束搜索,flag标记搜索成功或失败
     *
     * @param board 目标board
     */
    private void dfs(char[][] board) {
        //查询到list中下一个位置
        int nextPosition = getNextPosition();
        //找不到下一个尝试位置,即所有点均填充完成,则结束搜索,并判断为搜索成功
        if (nextPosition < 0) {
            flag = true;
            return;
        }
        //下一个位置的棋盘中的位置
        int next = list.get(nextPosition);
        int state = getState(next);
        //从1开始检索,检索到9
        int i = 0;
        while (++i < 10) {
            //开始尝试
            if ((state >> i) % 2 == 0) {//第i位为0,则证明该位置可能为i
                //更新行列
                board[next / 9][next % 9] = (char) (i + '0');
                list.set(nextPosition, -1);
                update(next / 9, next % 9, board);
//                out();
//                System.out.println("" + next / 9 + "    " + next % 9 + "    " + i);
                //开始搜索下一个位置
                dfs(board);
                //找到答案,结束搜索
                if (flag) {
                    return;
                }
                //未找到答案,撤回修改,继续尝试
                board[next / 9][next % 9] = '.';
                list.set(nextPosition, next);
                update(next / 9, next % 9, i, board);
            }
        }
    }

    /**
     * 获取下一个位置
     *
     * @return 下一个位置在list中的位置,若结果为-1则证明没有需要查询的结果
     */
    private int getNextPosition() {
        int position = -1;
        int minNum = -1;
        for (int i = 0; i < list.size(); i++) {
            //忽略被标记为-1的位置
            if (list.get(i) >= 0) {
                //找到可能性最少的位置
                int possibleNum = getPossibleNum(list.get(i));
                if (position < 0 || possibleNum < minNum) {
                    minNum = possibleNum;
                    position = i;
                }
            }
        }
        return position;
    }

    /**
     * 计算可能数字的数量
     */
    private int getPossibleNum(int position) {
        int state = getState(position);
        int num = 0;
        //遍历每个二进制位,若为1则计数器加1
        while (state > 0) {
            num += state & 1;
            state >>= 1;
        }
        return 9 - num;
    }

    /**
     * 查询某个位置的状态
     */
    private int getState(int position) {
        int x = position / 9;
        int y = position % 9;
        int state = col[x] | row[y] | block[x / 3][y / 3];
        return state;
    }

    /**
     * 更新某个位置的数据
     *
     * @param x     横坐标
     * @param y     纵坐标
     * @param board 棋盘
     */
    private void update(int x, int y, char[][] board) {
        int num = 1 << (board[x][y] - '0');
        col[x] |= num;
        row[y] |= num;
        block[x / 3][y / 3] |= num;
    }

    /**
     * 更新某个位置的数据到指定数字
     *
     * @param x      横坐标
     * @param y      纵坐标
     * @param target 目标数字
     * @param board  棋盘
     */
    private void update(int x, int y, int target, char[][] board) {
        int num = 1 << target;
        col[x] ^= num;
        row[y] ^= num;
        block[x / 3][y / 3] ^= num;
    }
}

执行结果:

性能:

记得关闭掉打印,否则会影响执行时间


后记

表面深搜,实则优化,提出解决方案并不难,提出优质的解决方案才是我们该追求的



作者:Echo_Ye

WX:Echo_YeZ

Email :echo_yezi@qq.com

个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

$flag 上一页 下一页