36. Valid Sudoku

数独问题和八皇后问题,都是回溯法(backtracking)的经典案例,就像LIS/LCS问题是DP的经典案例一样。

回溯法通俗点说就是不断试错,如果某一步走错了,就退回上一步重新选择。关键是要抽象出一个“状态”的概念,而且这个状态必须是可以rollback的。计算过程有点类似数学中“全排列”的概念。而且一般会用到递归。

数独的玩法很简单,用1~9填充所有的空格,规则只有3条:

  • 每一行不能有重复的数字
  • 每一列不能有重复的数字
  • 每一个小九宫格中,不能有重复的数字。比如下方的数独,有9个小九宫格,我们假设他们的编号是0-8。

我们先抛开这道题目,写一个通用的数独生成程序。对于理解数独和回溯法都有很大好处。

public class Solution {
    // 生成所有可能的9x9数独
    public void genSudoku() {
        // 这3个二维数组用于保存当前的状态
        // 如果rowFlag[i][j]==1,说明第i行已经有数字j出现过了,不能再选
        // 同样如果colFlag[i][j]==1,说明第i列已经有数字j出现过
        // cubeFlag[i][j]==1说明第i个小九宫格中已经有数字j出现过
        // 我为了方便将数组的size设为10,因为每个格子可能填写的数字是1-9,可以直接写rowFlag[3][9]=1之类的,要不还要手动处理数组边界问题
        int[][] rowFlag = new int[10][10];
        int[][] colFlag = new int[10][10];
        int[][] cubeFlag = new int[10][10];
        // 这个char[][]用于存储最终的结果
        char[][] board = new char[9][9];
        // 从[0,0]这个格子开始填写数字
        fillBoard(board, 0, 0, rowFlag, colFlag, cubeFlag);
    }

    public void fillBoard(char[][] board, int row, int col, int[][] rowFlag, int[][] colFlag, int[][] cubeFlag) {
        // 先计算出[row,col]这个元素,属于第几个小九宫格
        int cubeNum = (row / 3) * 3 + col / 3;
        // 这个格子可能填写1~9,一个个试过去
        for (int i = 1; i <= 9; i++) {
            // 判断当前格子是否可以填写数字i
            if (rowFlag[row][i] == 0 && colFlag[col][i] == 0 && cubeFlag[cubeNum][i] == 0) {
                // 如果可以填写数字i,就更新状态,并且继续尝试下一个格子
                board[row][col] = (char) ('0' + i);
                rowFlag[row][i] = 1;  // 注意这里的状态变化
                colFlag[col][i] = 1;
                cubeFlag[cubeNum][i] = 1;
                //System.out.println("choose " + i + " for cell (" + row + "," + col + ")");
                // 找出要填写的下一个格子,注意这里的判断条件
                // 我的填写顺序是从左向右,从上到下
                if (col < 8)
                    fillBoard(board, row, col + 1, rowFlag, colFlag, cubeFlag);
                else if (row < 8)
                    fillBoard(board, row + 1, 0, rowFlag, colFlag, cubeFlag);
                else
                    printBorad(board);   // 已经没有格子需要填了,当前格子是最后一个格子,说明当前board已经存储了一个正确的解法,打印出来

                // 别忘了状态的rollback,先把状态恢复过来,才能尝试下一个数字
                // 每次尝试时,状态必须是相同的
                rowFlag[row][i] = 0;
                colFlag[col][i] = 0;
                cubeFlag[cubeNum][i] = 0;
            }
        }
    }
    // debug用,输出一个数独
    public void printBorad(char[][] board) {
        for (int i = 0; i < 9; i++) {
            System.out.println(Arrays.toString(board[i]));
        }
        System.out.println(" ");
    }
}

理论上这段程序可以计算出所有可能的9x9数独,但我没想到解法居然有这么多。。。大概是10^21的级别,参考这篇文章Wiki。不过不要在意这些细节。。。关键是理解计算原理。

知道了原理,这道题目就非常简单了。这道题目只要求填入的数字满足那3条要求,甚至不要求有正确的解。对上面的数独生成程序稍微改改就可以了:

public class Solution {
    public boolean isValidSudoku(char[][] board) {
        // 其实一般人会用Map去存储状态吧,但我不知为何第一反应就是数组。。。
        int[][] rowFlag = new int[10][10];
        int[][] colFlag = new int[10][10];
        int[][] cubeFlag = new int[10][10];

        // 遍历给定的数独
        for (int row = 0; row < 9; row++) {
            for (int col = 0; col < 9; col++) {
                if (board[row][col] == '.')
                    continue;
                int cubeNum = (row / 3) * 3 + col / 3;
                // 当前格子填的数字是啥?
                // 注意下这种从char变int的方法
                int value = board[row][col] - '0';

                // 检查当前格子中的数字是否符合条件
                if (rowFlag[row][value] == 1 || colFlag[col][value] == 1 || cubeFlag[cubeNum][value] == 1)
                    return false;
                // 更新状态
                rowFlag[row][value] = 1;
                colFlag[col][value] = 1;
                cubeFlag[cubeNum][value] = 1;
            }
        }

        return true;
    }
}

results matching ""

    No results matching ""