题目描述

C051D746-16B0-4166-9ACD-3AD9535CB7B6_1_201_a.jpeg

题目链接

https://leetcode.cn/problems/sudoku-solver/

思路

前言
我们可以考虑按照「行优先」的顺序依次枚举每一个空白格中填的数字,通过「递归 + 回溯」的方法枚举所有可能的填法。当递归到最后一个空白格后,如果仍然没有冲突,说明我们找到了答案;在递归的过程中,如果当前的空白格不能填下任何一个数字,那么就进行回溯。

由于每个数字在同一行、同一列、同一个九宫格中只会出现一次,因此我们可以使用【37】解数独 - 图2【37】解数独 - 图3以及【37】解数独 - 图4这三个数组分别表示第【37】解数独 - 图5行,第【37】解数独 - 图6列,第【37】解数独 - 图7个九宫格中填写数字的情况。

:::warning 九宫格的范围为【37】解数独 - 图8以及【37】解数独 - 图9
具体地,第【37】解数独 - 图10行第【37】解数独 - 图11列的格子位于第【37】解数独 - 图12个九宫格中,其中【37】解数独 - 图13表示对【37】解数独 - 图14向下取整。 :::

实现
由于我们可以填写的数字范围为【37】解数独 - 图15,而数组的下标从【37】解数独 - 图16开始,因此在存储时,我们使用一个长度为【37】解数独 - 图17的布尔类型的数组,其中第【37】解数独 - 图18个元素的值为【37】解数独 - 图19时表示数字【37】解数独 - 图20出现过。例如我们用【37】解数独 - 图21表示数字【37】解数独 - 图22在第【37】解数独 - 图23行已经出现过,那么我们在遍历到第【37】解数独 - 图24行的空白格时,就不能填入数字【37】解数独 - 图25了。

我们首先对整个数独数组进行遍历,来记录数字的初始情况。当我们遍历到第【37】解数独 - 图26行第【37】解数独 - 图27列的位置时,如果该位置是一个数字【37】解数独 - 图28,那么我们需要将【37】解数独 - 图29【37】解数独 - 图30以及【37】解数独 - 图31均置为【37】解数独 - 图32

当我们结束了遍历过程后,就可以开始递归枚举。当递归到第【37】解数独 - 图33行第【37】解数独 - 图34列的位置时,我们枚举填入的数字【37】解数独 - 图35。根据题目的要求,数字【37】解数独 - 图36不能和当前行、列、九宫格中已经填入的数字相同,因此【37】解数独 - 图37【37】解数独 - 图38以及【37】解数独 - 图39必须均为【37】解数独 - 图40

当我们填入了数字【37】解数独 - 图41之后,我们要将上述的三个值都置为【37】解数独 - 图42,并且继续对下一个空白格位置进行递归。在回溯到当前递归层时,我们还要将上述的三个值重新置为【37】解数独 - 图43
未命名.png

代码实现

  1. public void solveSudoku(char[][] board) {
  2. // 遍历矩阵元素,记录初始值
  3. for (int i = 0; i < 9; i++) {
  4. for (int j = 0; j < 9; j++) {
  5. if (board[i][j] != '.') {
  6. int digit = board[i][j] - '1';
  7. line[i][digit] = column[j][digit] = block[i / 3][j / 3][digit] = true;
  8. }
  9. }
  10. }
  11. // 遍历矩阵元素,填充数独
  12. recur(board, 0, 0);
  13. }
  14. private final boolean[][] line = new boolean[9][9];
  15. private final boolean[][] column = new boolean[9][9];
  16. private final boolean[][][] block = new boolean[3][3][9];
  17. private boolean recur(char[][] board, int i, int j) {
  18. // 当前行已填充满,继续填充下一行
  19. if (j == board.length) {
  20. j = 0;
  21. i++;
  22. // 所有行已经全部填充完成
  23. if (i == board.length) {
  24. return true;
  25. }
  26. }
  27. // 当前元素不需要填充
  28. if (board[i][j] != '.') {
  29. return recur(board, i, j + 1);
  30. }
  31. // 遍历所有可用选择的数字,寻找一个符合当前要求的数字
  32. for (int digit = 0; digit < 9; digit++) {
  33. if (!line[i][digit] && !column[j][digit] && !block[i / 3][j / 3][digit]) {
  34. // 填充数字
  35. board[i][j] = (char) (digit + '0' + 1);
  36. line[i][digit] = column[j][digit] = block[i / 3][j / 3][digit] = true;
  37. // 递归
  38. if (recur(board, i, j + 1)) {
  39. return true;
  40. }
  41. // 回溯
  42. board[i][j] = '.';
  43. line[i][digit] = column[j][digit] = block[i / 3][j / 3][digit] = false;
  44. }
  45. }
  46. return false;
  47. }

复杂度分析

由于这些方法均以「递归 + 回溯」为基础,算法运行的时间(以及时间复杂度)很大程度取决于给定的输入数据,而我们很难找到一个非常精确的渐进紧界。因此这里只给出一个较为宽松的渐进复杂度上界【37】解数独 - 图45,即最多有【37】解数独 - 图46个空白格,每个格子可以填【37】解数独 - 图47中的任意整数。