zoukankan      html  css  js  c++  java
  • 【算法刷题】解数独

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


    概览

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

    难度:中等

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


    题目来源

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


    题目内容

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

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

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

    本题满足以下设定:

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

    样例

    源数据

    img

    结果

    img

    解题思路

    • 基本思路:显然可以使用递归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;
        }
    }
    

    执行结果:

    image-20200916163654701

    性能:

    image-20200916164057937

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


    后记

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



    作者:Echo_Ye

    WX:Echo_YeZ

    Email :echo_yezi@qq.com

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

  • 相关阅读:
    css 冷知识
    js跨域问题
    检测浏览器是否支持ES6
    新增项目到GIT仓库中
    如何正确设置统计博客园的个人博客访问量(图文详解)
    Node中包的加载机制
    图片路径中含有中文在jsp下不能正常显示的问题
    通过Xshell来访问和连接Linux
    文件下载该注意的问题
    文件上传该注意的问题
  • 原文地址:https://www.cnblogs.com/silent-bug/p/13680052.html
Copyright © 2011-2022 走看看