LeetCode算法题4.1:递归和回溯-解数独

文章目录

  • 解数独
    • 回溯 :
    • 仅仅在实现方式上有区别
  • 总结


解数独

      题目链接:https://leetcode-cn.com/problems/sudoku-solver/

      题目描述:编写一个程序,通过填充空格来解决数独问题。

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

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。

回溯 :

      思路,运用回溯算法的思想,在 N 皇后问题的基础上主要解决一个问题:如何描述限制条件?每次在一个位置上放置元素时,需要满足同行同列和所在九宫格内没有重复元素,那么可以用 Set 集合来描述这个限制条件。
      所以回溯类型的题目都不能判断出当前解是否为最优解,而是只能得到一个合适的解,对于本题而言,已明确告诉仅有一个唯一解,所以直接结束即可。参考代码如下(带注释):

	boolean f=false;//这个全局变量是为了让程序在得到唯一解的时候进行递归的一层层返回,不再进行多余的无关的操作。
    public void solveSudoku(char[][] board) {
        List<Set<Character>> rows = new ArrayList<>(9);// 9 个Set用来保存每一行已有的数,Set后面用来判断后面是否有重复
        List<Set<Character>> columns = new ArrayList<>(9);// 同上,保存每一列已有的数
        List<Set<Character>> grid9 = new ArrayList<>(9);// 同上,保存每一个小九宫格中已有的数,九宫格从左到右,再从上到下为:0,1,2   3,4,5   6,7,8
        Set<Character> row = new HashSet<>();
        Set<Character> column = new HashSet<>();

        List<int[]> m = new ArrayList<>();//保存所有'.'的坐标,将所有待填充数的位置坐标保存到列表中。

        Set<Character> g1 = new HashSet<>();
        Set<Character> g2 = new HashSet<>();
        Set<Character> g3 = new HashSet<>();
        //对所有集合均进行初始化
        for (int i = 0; i < 9; i++) {//初始化 rows、columns 和 grid9 .
            for (int j = 0; j < 9; j++) {
                if (board[i][j] != '.') {
                    row.add(board[i][j]);//初始化 rows
                    //初始化 grid9
                    if (j < 3)
                        g1.add(board[i][j]);
                    else if (j < 6)
                        g2.add(board[i][j]);
                    else
                        g3.add(board[i][j]);
                } else
                    m.add(new int[]{i, j});//初始化 m
                if (board[j][i] != '.')
                    column.add(board[j][i]);//初始化 columns

            }
            if ((i + 1) % 3 == 0) {
                grid9.add(new HashSet<>(g1));
                grid9.add(new HashSet<>(g2));
                grid9.add(new HashSet<>(g3));
                g1.clear();
                g2.clear();
                g3.clear();
            }
            rows.add(new HashSet<>(row));
            columns.add(new HashSet<>(column));
            row.clear();
            column.clear();
        }
         //初始化完成
        solve(m, board, rows, columns, grid9, m.size(), 0);
    }
        public void solve(List<int[]> m, char[][] board, List<Set<Character>> rows, List<Set<Character>> columns, List<Set<Character>> grid9, int nums, int count) {
        if (count == nums) {//当最后一个位置上的数已经填充好之后,此时需要返回,标记f为true,表示已找到唯一解了
            f=true;
            return ;
        }
        int i = m.get(count)[0], j = m.get(count)[1];//得到当前需要填充的行列坐标
        for (Character k = '1'; k <= '9'; k++) {
        	//依照题意,判断当前数 k 是否合法,即集合中是否已经有重复的数
            if (rows.get(i).contains(k))
                continue;
            if (columns.get(j).contains(k))
                continue;
            int location = (i / 3)*3 + j / 3;//计算在 i,j处的元素处于哪个九宫格。
            if (grid9.get(location).contains(k))
                continue;
			//找到一个可行的 k 时,分别添加进三个集合
            rows.get(i).add(k);
            columns.get(j).add(k);
            grid9.get(location).add(k);
            
            board[i][j] = k;//设置 board[i][j] 的值为k
            
            solve(m, board, rows, columns, grid9, nums, count + 1);//count加一,在下一个位置上进行填充
            /*
            if(!f){//如果找到了唯一解(f为true),solve 函数需要一层层直接返回,后面这些代码不应该被执行。
                board[i][j] = '.'; //回退到上一个状态,并从集合里移出该 k 值。
                rows.get(i).remove(k);
                columns.get(j).remove(k);
                grid9.get(location).remove(k);
            }*/
            
            if(f)
            	break;
            board[i][j] = '.';
            rows.get(i).remove(k);
            columns.get(j).remove(k);
            grid9.get(location).remove(k);
        }
    }

       有些东西需要说明:上面算法中,若是去掉全局变量 f 和相关的语句的话,程序执行完毕之后,board 的值就会又变为初始状态,状态变化为:初始状态 -> 找到唯一解 -> 又回到初始状态。并且程序在接下来做的工作为"试图找到下一个可行的解“,但实际上并没有可行的解了,所做的均是无用功。 本题和之前的回溯算法对比,比如 N-皇后问题,区别在于是否只有一个唯一解,但这也不是最大的区别,最大的区别在于 N-皇后这些问题在找到一个可能的解之后是将该解保存下来了,而本题没有采用这种做法,虽然也可以这样做,但没必要(没必要浪费时间)。

       加上 f 之后,因为不再执行赋值语句了,所以 board 中的元素一直都保持唯一解不变,并且三个集合也不会在移出元素,所以后面的 for 循环会找不到任何一个可行的 k ,程序便会由此一步步退出。其实直接加上 break 语句更好,这样后面的 for 循环直接得不到执行的机会了,更加省事,如上面代码所示。

仅仅在实现方式上有区别

      此文链接:https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247492800&idx=2&sn=47bf7232fe5fd533aaf683850f3bd5c7&scene=21#wechat_redirect 它和自己实现的代码在思想基本差不多,但是其代码实现方式可供参考借鉴。如下:

public void solveSudoku(char[][] board) {
        backtrack(board,0,0);//从位置(0,0)开始判断
    }
    boolean backtrack(char[][] board, int i, int j) {
        int m = 9, n = 9;
        if (j == n) {
            // 穷举到最后一列的话就换到下一行重新开始。
            return backtrack(board, i + 1, 0);
        }
        if (i == m) {
            // 找到一个可行解,触发 base case
            return true;
        }

        if (board[i][j] != '.') {
            // 如果有预设数字,不用我们穷举
            return backtrack(board, i, j + 1);
        }

        for (char ch = '1'; ch <= '9'; ch++) {
            // 如果遇到不合法的数字,就跳过
            if (!isValid(board, i, j, ch))
                continue;

            board[i][j] = ch;
            // 如果找到一个可行解,立即结束
            if (backtrack(board, i, j + 1)) {
                return true;
            }
            board[i][j] = '.';
        }
        // 穷举完 1~9,依然没有找到可行解,此路不通
        return false;
    }

    boolean isValid(char[][] board, int r, int c, char n) {
        for (int i = 0; i < 9; i++) {
            // 判断行是否存在重复
            if (board[r][i] == n) return false;
            // 判断列是否存在重复
            if (board[i][c] == n) return false;
            // 判断 3 x 3 方框是否存在重复
            if (board[(r/3)*3 + i/3][(c/3)*3 + i%3] == n)
                return false;
        }
        return true;
    }

总结

      对了,还有本题的官方题解可供参考,自己实现的代码时间复杂度有点高…

你可能感兴趣的:(数据结构与算法,算法,leetcode)