题目描述

请编写一个程序,给数独中剩余的空格填写上数字,空格用字符'.'来表示,假设给定的数独只有唯一的解法。
示例1:
输入:
[[.,.,9,7,4,8,.,.,.],[7,.,.,.,.,.,.,.,.],[.,2,.,1,.,9,.,.,.],[.,.,7,.,.,.,2,4,.],[.,6,4,.,1,.,5,9,.],[.,9,8,.,.,.,3,.,.],[.,.,.,8,.,3,.,2,.],[.,.,.,.,.,.,.,.,6],[.,.,.,2,7,5,9,.,.]]

返回值:
[[5,1,9,7,4,8,6,3,2],[7,8,3,6,5,2,4,1,9],[4,2,6,1,3,9,8,7,5],[3,5,7,9,8,6,2,4,1],[2,6,4,3,1,7,5,9,8],[1,9,8,5,2,4,3,6,7],[9,7,5,8,6,3,1,2,4],[8,3,2,4,9,1,7,5,6],[6,4,1,2,7,5,9,8,3]]

题目分析

算法获取数独的结果,计算过程就是将空格中的数字进行1~9的枚举,当该数字满足横向、列向、块内与已出现的数字不重复,则进行下一个空格的数字枚举,直到所有的空格都选择了满足条件的数字,数独完成,如图示例1:

alt

解题思路

方法1:dfs递归回溯,暴力法判断

dfs递归回溯,对数组进行遍历,碰到空格就进行1~9的数字选择,同时也要对数字进行遍历判断是否与已经出现的数字重复,判断的方法可以直接采用暴力遍历该位置的同行、列、块的所有数据,dfs的过程如下:

dfs(){
   // 顺序遍历数组,找到空格的位置x,y
   // 对空格位置进行1~9的选择
   for(int i=1;i<=9;i++){
      // 判断选择的数字是否满足条件
      // 满足 dfs下一个空格,直到所有空格都被填上数字
      // 不满足跳过
   }
}
// 判断数字是否符合条件
isValid(x, y){
     // 循环该行(x确定)的所有数字,看是否重复
     for(int i=0;i<9;i++) board[x][i];
     // 循环该列(y确定)的所有数字,看是否重复
     for(int i=0;i<9;i++) board[i][y];
     // 循环该块的所有数字,看是否重复(这里对块进行的编号,从左到右,从上到下)
     for(int i=0;i<9;i++) board[x/3*3+i/3][y/3*3+i%3];
}

方法2:dfs递归回溯,使用set判断

使用set记录每行每列每个块已经出现过的数字,可以加快判断数字是否满足条件的速度。因为有些数字已经遍历过,但每次判断都要重新遍历一遍,会增加很多时间,通过使用set来存储已知的数字,在判断数字上由原来的O(n)优化到O(1)(hashset);

具体需要使用3n个set来存储n行,n列,n个块对应的数字。

代码实现

方法1:dfs递归回溯,暴力法判断

import java.util.*;
public class Solution {
    boolean flag = false;
    public void solveSudoku(char[][] board) {
        // 进行递归回溯
        dfs(board, 0, 0);
    }
    
    public void dfs(char[][] board, int i, int j){
        if(i == board.length){
            // 已经遍历所有的数,说明已经获得结果
            flag = true;
            return ;
        }
        // 找到第i行中要填的数
        while(j<board.length && board[i][j] != '.'){
            j++;
        }
        if(j == board.length){
            // 遍历完一行数,遍历下一行
            dfs(board, i+1, 0);
        }
        if(i<board.length && j<board.length){
            for(char x='1';x<='9';x++){
                 // 不符合数独条件
                if(!isValid(board, i, j, x)) continue;
                // 进行选择
                board[i][j] = x;
                dfs(board, i, j+1);
                // 如果已经完成了选择,可以不用撤销选择,也不用再遍历后面的情况
                if(flag) break;
                // 撤销选择
                board[i][j] = '.';
            }
        }
    }
    
    public boolean isValid(char[][] board, int x, int y, char ch){
        for(int i=0;i<board.length;i++){
            // 判断以x,y为坐标的行、列、块中是否存在字符ch
            if(board[x][i] == ch) return false;
            if(board[i][y] == ch) return false;
            if(board[x/3*3+i/3][y/3*3+i%3] == ch) return false;
        }
        return true;
    }
}

时间复杂度:最差为O(nm))O(n^m)),n是数独的数组长度(题目中只设定为9),m是空格的数量加1,对每一个空格都需要列举n种情况,但实际上不会对所有情况进行穷举,只要某一种情况下能够满足所有数字条件,就会直接返回结果,因为使用了暴力判断,所以每次数字的遍历都需要n的时间来判断,所以额外加一个n的乘积;

空间复杂度:最差为O(n2)O(n^2),n是数独的数组长度,,最多需要n2n^2的递归空间。

方法2:dfs递归回溯,使用set判断

import java.util.*;
public class Solution {
    // 记录每行、每列、每个3X3块中已经出现的数字
    List<Set<Character>> row = new ArrayList<>();
    List<Set<Character>> col = new ArrayList<>();
    List<Set<Character>> block = new ArrayList<>();
    boolean flag = false;
    public void solveSudoku(char[][] board) {
        for(int i=0;i<board.length;i++){
            // 创建对应的set
            row.add(new HashSet<Character>());
            col.add(new HashSet<Character>());
            block.add(new HashSet<Character>());
        }
        for(int i=0;i<board.length;i++){
            for(int j=0;j<board.length;j++){
                if(board[i][j] != '.'){
                    // 先将已知的数字填入对应的set中
                    row.get(i).add(board[i][j]);
                    col.get(j).add(board[i][j]);
                    block.get(i/3*3+j/3).add(board[i][j]);
                }
            }
        }
        dfs(board, 0, 0);
    }
    
    public void dfs(char[][] board, int i, int j){
        if(i == board.length){
            // 已经遍历所有的数
            flag = true;
            return ;
        }
        // 找到第i行中要填的数(空格)
        while(i<board.length && j<board.length && board[i][j] != '.'){
            j++;
        }
        if(j == board.length){
            // 遍历完一行数,遍历下一行
            dfs(board, i+1, 0);
        }
        for(char x='1';x<='9';x++){
            int bi = i/3*3+j/3;
            // 符合数独条件
            if(!row.get(i).contains(x) && !col.get(j).contains(x) && !block.get(bi).contains(x)){
                // 进行选择
                board[i][j] = x;
                row.get(i).add(board[i][j]);
                col.get(j).add(board[i][j]);
                block.get(bi).add(board[i][j]);
                dfs(board, i, j+1);
                if(flag) break;
                // 撤销选择
                row.get(i).remove(board[i][j]);
                col.get(j).remove(board[i][j]);
                block.get(bi).remove(board[i][j]);
                board[i][j] = '.';
            }
        }
    }
}

时间复杂度:最差为O(nm)O(n^m),n是数独的数组长度(题目中只设定为9),m是空格的数量,对每一个空格都需要列举n种情况,但实际上不会对所有情况进行穷举,只要某一种情况下能够满足所有数字条件,就会直接返回结果,因为使用了set来加快数字的判断,所以时间上会比暴力法判断快;

空间复杂度:O(n2)O(n^2),n是数独的数组长度,,在遍历的过程中使用了存储所有数字的set(row, col, block),存储的数据大小为n2n^2,同时最多需要n2n^2的递归空间。