LeetCode 37. 解数独

3 阅读4分钟

LeetCode 37. 解数独

摘要

LeetCode 37 解数独题解,采用回溯法在决策树上深度优先遍历,通过行、列、九宫格三个哈希表剪枝冲突位置,递归填充空白格,时间复杂度 O(9^(空格数)),空间复杂度 O(1)(固定 9×9 棋盘)。

标签

#算法 #LeetCode #回溯 #DFS #矩阵 #剪枝 #题解

目录

  1. 题目描述

  2. 易错点

  3. 思路

    • 决策树模型
    • 剪枝条件
    • 回溯流程
  4. 编写代码


1. 题目描述

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

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

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

数独部分空格已填,空白格用 '.' 表示。

示例 1:

text

输入:board = [  ["5","3",".",".","7",".",".",".","."],
  ["6",".",".","1","9","5",".",".","."],
  [".","9","8",".",".",".",".","6","."],
  ["8",".",".",".","6",".",".",".","3"],
  ["4",".",".","8",".","3",".",".","1"],
  ["7",".",".",".","2",".",".",".","6"],
  [".","6",".",".",".",".","2","8","."],
  [".",".",".","4","1","9",".",".","5"],
  [".",".",".",".","8",".",".","7","9"]
]
输出:[  ["5","3","4","6","7","8","9","1","2"],
  ["6","7","2","1","9","5","3","4","8"],
  ["1","9","8","3","4","2","5","6","7"],
  ["8","5","9","7","6","1","4","2","3"],
  ["4","2","6","8","5","3","7","9","1"],
  ["7","1","3","9","2","4","8","5","6"],
  ["9","6","1","5","3","7","2","8","4"],
  ["2","8","7","4","1","9","6","3","5"],
  ["3","4","5","2","8","6","1","7","9"]
]

提示:

  • board.length == 9
  • board[i].length == 9
  • board[i][j] 是一位数字或 '.'
  • 题目数据保证输入数独仅有一个解

2. 易错点

  1. 递归返回值的处理backtrack 函数必须返回 boolean,一旦找到可行解立即逐层返回 true,避免继续搜索。
  2. 九宫格索引计算box = Math.floor(row / 3) * 3 + Math.floor(col / 3),范围 [0, 8]
  3. 状态恢复:回溯时需将 board[row][col] 恢复为 '.',同时将三个标记数组置为 false
  4. 跳过已有数字:若当前位置已有数字,直接递归下一个位置,不进行尝试。
  5. 结束条件pos === 81 表示所有格子处理完毕,返回 true
  6. 标记数组维度:行、列、九宫格各为 9×10(数字 1-9),索引使用数字值,0 位不使用。

3. 思路

决策树模型

数独的决策树每个节点对应一个空白格(按行优先顺序从 0 到 80)。每个节点的分支是 1~9 的候选数字,但只有通过剪枝条件(行、列、宫不冲突)的数字才能进入下一层。当所有空白格都被填满(pos === 81)时,得到一个合法解。

与普通回溯题不同,数独的答案唯一,因此一旦找到解即可立即返回,无需枚举所有解。

剪枝条件

在尝试放置数字 num 之前,检查:

  • rows[row][num]:第 row 行是否已有 num
  • cols[col][num]:第 col 列是否已有 num
  • boxes[box][num]:所在九宫格是否已有 num

任意为 true 则跳过该数字。

回溯流程

  1. 初始化标记数组:遍历棋盘,将已有数字记录到行、列、九宫格标记中。

  2. 定义递归函数 backtrack(pos)

    • 若 pos === 81,返回 true(全部填满)。

    • 计算当前格子坐标 row = Math.floor(pos / 9)col = pos % 9

    • 若 board[row][col] !== '.',直接递归 backtrack(pos + 1)

    • 否则,尝试数字 1~9:

      • 检查是否冲突,冲突则跳过。
      • 放置数字,更新标记。
      • 递归 backtrack(pos + 1),若返回 true,立即返回 true
      • 否则撤销放置(回溯),继续尝试下一个数字。
    • 若所有数字尝试均失败,返回 false

  3. 从 pos = 0 开始递归。

该方法每个空格最多尝试 9 个数字,但剪枝会大幅减少实际搜索量。由于数独唯一解,找到即停。

复杂度

  • 时间复杂度:最坏 O(9^m),m 为空格数,但实际剪枝后远小于此。
  • 空间复杂度:O(1),固定 9×9 棋盘和常数大小的标记数组。

4. 编写代码

javascript 运行

javascript

var solveSudoku = function(board) {
    const rows = Array.from({ length: 9 }, () => Array(10).fill(false));
    const cols = Array.from({ length: 9 }, () => Array(10).fill(false));
    const boxes = Array.from({ length: 9 }, () => Array(10).fill(false));

    for (let i = 0; i < 9; i++) {
        for (let j = 0; j < 9; j++) {
            if (board[i][j] !== '.') {
                const num = parseInt(board[i][j]);
                rows[i][num] = true;
                cols[j][num] = true;
                const box = Math.floor(i / 3) * 3 + Math.floor(j / 3);
                boxes[box][num] = true;
            }
        }
    }

    const backtrack = (pos) => {
        if (pos === 81) return true;

        const row = Math.floor(pos / 9);
        const col = pos % 9;
        const box = Math.floor(row / 3) * 3 + Math.floor(col / 3);

        if (board[row][col] !== '.') {
            return backtrack(pos + 1);
        }

        for (let num = 1; num <= 9; num++) {
            if (rows[row][num] || cols[col][num] || boxes[box][num]) continue;

            board[row][col] = String(num);
            rows[row][num] = true;
            cols[col][num] = true;
            boxes[box][num] = true;

            if (backtrack(pos + 1)) return true;

            rows[row][num] = false;
            cols[col][num] = false;
            boxes[box][num] = false;
            board[row][col] = '.';
        }
        return false;
    };

    backtrack(0);
};