【LeetCode 37】解数独 (Sudoku Solver) —— 回溯法详解 (Python/C/C++)

题目描述

编写一个程序,通过填充空格来解决数独问题。 数独的解法需 遵循如下规则:

  1. 数字 1-9 在每一行只能出现一次。

  2. 数字 1-9 在每一列只能出现一次。

  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 数独部分空格内已填入了数字,空白格用 '.' 表示。

提示:

  • 给定的数独序列只包含数字 1-9 和字符 '.'

  • 你可以假设给定的数独只有唯一解。

  • 给定数独永远是 9x9 形式的。


思路解析

解数独 这道题,棋盘的每一个位置都要放数字,我们需要去试探每一个空位放 1-9 究竟合不合适。因此,解数独是一个二维递归 问题! 我们需要两个 for 循环遍历棋盘的行和列,然后再用一个递归函数去控制每个位置放入的数字。

1. 回溯函数返回值设计 (关键点)

注意,这是解数独问题最容易踩坑的地方:回溯函数的返回值必须是 bool 类型!

之前我们做组合问题、排列问题,找到所有满足条件的集合即可,所以返回值为 void。但数独问题只需要找到一个符合条件的解即可 (题目说了只有唯一解)。 因为只需要找到一个解,当我们到达叶子节点(也就是把所有空位填满且合法)时,就立刻返回 true,层层向上传递,停止后面的所有搜索。

2. 回溯三部曲

  • 递归函数及参数 需要传入完整的棋盘 board。返回值刚才说了,是 bool 类型。

    cpp 复制代码
    bool backtracking(vector<vector<char>>& board)
  • 递归终止条件 其实不需要显式地写终止条件。因为我们的两个 for 循环会遍历整个棋盘。如果遍历完了所有的空位,都没有返回 false,说明我们成功把棋盘填满了,此时直接在函数末尾返回 true 即可。

  • 单层搜索逻辑 外层两个 for 循环遍历行和列,找到空位 .。 内层再用一个 for 循环枚举 1-9。 判断如果把这个数字放进去是合法的,那就放入该数字,并立刻进入下一层递归 if (backtracking(board)) return true;。 如果递归发现不对,就回溯,把数字改回 .。 如果 1-9 全都试过了都不行,说明当前的分支是死路,直接 return false;

3. 判断棋盘是否合法

我们需要写一个 isValid 函数,专门用来判断在 board[row][col] 放入数字 val 是否合法。 检查三个维度:

  1. 同行是否重复

  2. 同列是否重复

  3. 同一个 3x3 九宫格 是否重复(计算九宫格左上角起点的公式:startRow = (row / 3) * 3

偷一下卡哥的图

代码实现

下面提供 Python, C, C++ 三个版本的代码,核心逻辑完全一致。

C++ 版本

cpp 复制代码
class Solution {
private:
    bool row[9][10];
    bool col[9][10];
    bool block[3][3][10];
    vector<pair<int, int>> spaces;

    bool dfs(int pos, vector<vector<char>>& board) {
        if (pos == spaces.size()) {
            return true; // 填满所有空位
        }

        auto [i, j] = spaces[pos];
        
        for (int digit = 1; digit <= 9; ++digit) {
            if (!row[i][digit] && !col[j][digit] && !block[i / 3][j / 3][digit]) {
                // 做选择
                row[i][digit] = col[j][digit] = block[i / 3][j / 3][digit] = true;
                board[i][j] = digit + '0';
                
                if (dfs(pos + 1, board)) return true;
                
                // 撤销选择
                row[i][digit] = col[j][digit] = block[i / 3][j / 3][digit] = false;
            }
        }
        return false;
    }

public:
    void solveSudoku(vector<vector<char>>& board) {
        memset(row, false, sizeof(row));
        memset(col, false, sizeof(col));
        memset(block, false, sizeof(block));
        spaces.clear();

        for (int i = 0; i < 9; ++i) {
            for (int j = 0; j < 9; ++j) {
                if (board[i][j] == '.') {
                    spaces.emplace_back(i, j);
                } else {
                    int digit = board[i][j] - '0';
                    row[i][digit] = true;
                    col[j][digit] = true;
                    block[i / 3][j / 3][digit] = true;
                }
            }
        }

        dfs(0, board);
    }
};

C 语言版本

C语言在传递二维数组时使用了双重指针,逻辑与C++一致,纯底层控制:

objectivec 复制代码
#include <stdbool.h>
#include <string.h>

bool row[9][10];
bool col[9][10];
bool block[3][3][10];

// 用来保存空位坐标的结构体数组
struct {
    int i, j;
} spaces[81];
int space_count;

bool dfs(int pos, char** board) {
    if (pos == space_count) {
        return true;
    }

    int i = spaces[pos].i;
    int j = spaces[pos].j;

    for (int digit = 1; digit <= 9; ++digit) {
        if (!row[i][digit] && !col[j][digit] && !block[i / 3][j / 3][digit]) {
            // 选择
            row[i][digit] = true;
            col[j][digit] = true;
            block[i / 3][j / 3][digit] = true;
            board[i][j] = digit + '0';

            if (dfs(pos + 1, board)) return true;

            // 回溯
            row[i][digit] = false;
            col[j][digit] = false;
            block[i / 3][j / 3][digit] = false;
        }
    }
    return false;
}

void solveSudoku(char** board, int boardSize, int* boardColSize) {
    memset(row, false, sizeof(row));
    memset(col, false, sizeof(col));
    memset(block, false, sizeof(block));
    space_count = 0;

    for (int i = 0; i < 9; ++i) {
        for (int j = 0; j < 9; ++j) {
            if (board[i][j] == '.') {
                spaces[space_count].i = i;
                spaces[space_count].j = j;
                space_count++;
            } else {
                int digit = board[i][j] - '0';
                row[i][digit] = true;
                col[j][digit] = true;
                block[i / 3][j / 3][digit] = true;
            }
        }
    }

    dfs(0, board);
}

Python 版本

python 复制代码
class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        # 记录某行、某列、某九宫格中,数字 1-9 是否已存在
        row = [[False] * 10 for _ in range(9)]
        col = [[False] * 10 for _ in range(9)]
        block = [[[False] * 10 for _ in range(3)] for _ in range(3)]
        spaces = [] # 专门记录所有空白格的坐标

        # 1. 预处理:初始化状态数组,并收集所有空位
        for i in range(9):
            for j in range(9):
                if board[i][j] == '.':
                    spaces.append((i, j))
                else:
                    digit = int(board[i][j])
                    row[i][digit] = True
                    col[j][digit] = True
                    block[i // 3][j // 3][digit] = True

        # 2. DFS 回溯
        def dfs(pos: int) -> bool:
            # 如果所有的空位都填满了,说明找到了解
            if pos == len(spaces):
                return True
            
            i, j = spaces[pos]
            
            # 尝试填入 1-9
            for digit in range(1, 10):
                # O(1) 判断是否合法
                if not row[i][digit] and not col[j][digit] and not block[i // 3][j // 3][digit]:
                    # 做出选择
                    row[i][digit] = col[j][digit] = block[i // 3][j // 3][digit] = True
                    board[i][j] = str(digit)
                    
                    # 进入下一层递归
                    if dfs(pos + 1):
                        return True
                        
                    # 撤销选择(回溯)
                    row[i][digit] = col[j][digit] = block[i // 3][j // 3][digit] = False
                    
            return False

        dfs(0)

问题:

python 复制代码
block_used = [[[False] * 10 for _ in range(3)] for _ in range(3)]啥意思呢

这行代码确实是 Python 里比较容易让人头晕的嵌套列表推导式 (List Comprehension)。别急,我们把它像剥洋葱一样,从里到外一层一层拆开来看。

它的最终目的是:创建一个 3 维(三维)数组,用来记录数独中 9 个「3x3 的九宫格」里,数字 1 到 9 是否已经出现过。

我们可以把它拆成三个步骤来理解:

第一层(最里面):[False] * 10

  • 代码意思 :创建一个包含 10 个 False 的一维列表。

  • 长这样[False, False, False, False, False, False, False, False, False, False]

  • 为什么是 10 个而不是 9 个?

    数独虽然只有数字 1-9,但编程中数组的索引是从 0 开始的。为了方便,我们直接把数组大小开到 10。这样我们就可以直接用索引 19 来代表数独里的数字 1-9,而索引 0 直接废弃不用,省去了每次都要减 1 的计算麻烦。

  • 它的作用 :这代表某一个特定的九宫格 里,数字 1-9 的使用状态。全是 False 表示现在全是空的。


第二层(中间):[ [...] for _ in range(3) ]

  • 代码意思:把刚才那个长度为 10 的一维列表,循环创建 3 次,装进一个大列表里。

  • 长这样(一个二维数组):

    python 复制代码
    [
        [False, False, ..., False], # 代表第 0 列的九宫格
        [False, False, ..., False], # 代表第 1 列的九宫格
        [False, False, ..., False]  # 代表第 2 列的九宫格
    ]
  • 它的作用:数独棋盘的横向有 3 个大九宫格。这一层生成了一整行的 3 个九宫格的状态表。


第三层(最外面):[ [...] for _ in range(3) ]

  • 代码意思:把刚才那一整行的状态表(二维数组),再循环创建 3 次,装进一个更大的列表里。

  • 长这样(一个三维数组):

    python 复制代码
    [
        # 第 0 行的 3 个九宫格
        [ [10个False], [10个False], [10个False] ], 
    
        # 第 1 行的 3 个九宫格
        [ [10个False], [10个False], [10个False] ], 
    
        # 第 2 行的 3 个九宫格
        [ [10个False], [10个False], [10个False] ]  
    ]
  • 它的作用:完美对应了数独棋盘的 3 * 3 个大九宫格(总共 9 个)。


💡 结合坐标怎么使用这个三维数组?

理解了它的结构,我们再看看代码里是怎么用的:block_used[i // 3][j // 3][num]

假设我们现在在棋盘的坐标 (i, j) 上,也就是第 i 行第 j 列(ij 的范围都是 0 到 8),我们想填入数字 num(比如数字 7)。

  1. i // 3 :计算出我们当前所在的格子,属于大九宫格的哪一行 。例如 i=5(第5行),5 // 3 = 1,说明属于第 1 行的大九宫格(中间那行)。

  2. j // 3 :计算出我们所在的格子,属于大九宫格的哪一列 。例如 j=7(第7列),7 // 3 = 2,说明属于第 2 列的大九宫格(最右边那列)。

  3. [num] :直接去查状态表里索引为 num 的位置是 True 还是 False

连起来 block_used[1][2][7] 的意思就是:去查数独盘面上,第 1 行、第 2 列的那个大九宫格里面,数字 7 是否已经被填过了。(C与C++同理)


复杂度分析

  • 时间复杂度 : O(9^m),其中 m 是数独中空白格的数量。由于每个空白格最多有 9 种可能的数字填入,最坏情况下的分支因子为 9。但实际上由于加入了严格的合法性剪枝 (isValid 函数),实际运行时间远远小于理论上限。

  • 空间复杂度: O(1),如果不考虑系统递归调用栈所消耗的空间的话。由于数独固定大小为 9x9,递归深度最大为 81,所以空间复杂度也是常数级别的。

总结

解数独是回溯算法体系里非常硬核的一道题目,它突破了我们在一维数组上回溯的惯性思维,引入了二维数组的遍历与嵌套递归。

只要牢牢把握住:bool 返回值来截断后续多余的搜索分支 ,并且把 检查行、列、3x3 宫格的合法性函数 写对,这道 Hard 题目其实就像套模板一样清晰!

照例贴上卡哥的代码随想录

37. 解数独 | 回溯法 | 二维递归 | 代码随想录-全网最全算法数据结构刷题学习路线|图文+视频教程|免费开源

相关推荐
热心网友俣先生1 小时前
2026年第二十三届五一数学建模竞赛B题四问参考答案+多算法对比
算法·数学建模
HAPPY酷1 小时前
从Public到Private:UE5 C++类创建路径差异全解析
java·c++·ue5
AI进化营-智能译站1 小时前
ROS2 C++开发系列08-传感器数据缓存与指令解析方式之数组、向量与字符串实战
开发语言·c++·缓存·ai
风流 少年1 小时前
Python Web框架:FastAPI
前端·python·fastapi
风筝在晴天搁浅1 小时前
LeetCode 162.寻找峰值
算法·leetcode
Qres8211 小时前
Rabrg/artificial-life test
python·模拟
AI进化营-智能译站1 小时前
ROS2 C++开发系列14-Lambda表达式处理传感器数据流|文件IO保存机器人实验日志
开发语言·c++·ai·机器人
itzixiao2 小时前
L1-067 洛希极限(10分)[java][python]
java·开发语言·算法
财经资讯数据_灵砚智能2 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年5月1日
大数据·人工智能·python·信息可视化·自然语言处理