LeetCode 37:解数独

LeetCode 37:解数独

在这里插入图片描述

问题定义与核心挑战

数独规则:

  1. 数字 1-9 在每一行、每一列、每个 3×3 宫格内仅出现一次
  2. 输入棋盘含空单元格(用 . 表示),需填充为合法数独。

核心挑战:

  • 解空间大:直接暴力枚举所有可能会超时,需通过回溯 + 剪枝高效搜索。
  • 约束检查:如何快速判断当前数字是否违反行、列、宫格的唯一性约束。

核心思路:回溯法 + 标记数组

  1. 标记数组:维护三个二维数组(rowcolbox),分别记录行、列、宫格中数字的出现情况,快速判断约束。
  2. 回溯填充:遍历每个空单元格,尝试填入 1-9,若合法则递归处理下一个单元格;若递归失败则回溯(恢复状态,尝试其他数字)。

算法步骤详解

步骤 1:初始化标记数组
  • row[i][num]:第 i 行是否已存在数字 numnum ∈ [1,9])。
  • col[j][num]:第 j 列是否已存在数字 num
  • box[boxIndex][num]:第 boxIndex 个宫格是否已存在数字 num(宫格索引 boxIndex = (i/3)*3 + j/3)。

遍历初始棋盘,标记已有数字:

for (int i = 0; i < 9; i++) {
    for (int j = 0; j < 9; j++) {
        if (board[i][j] != '.') {
            int num = board[i][j] - '0';
            int boxIndex = (i / 3) * 3 + (j / 3); // 计算宫格索引
            row[i][num] = true;
            col[j][num] = true;
            box[boxIndex][num] = true;
        }
    }
}
步骤 2:回溯函数设计

递归填充空单元格,核心逻辑:

  1. 寻找空单元格:遍历棋盘,找到第一个值为 . 的位置 (i,j)
  2. 尝试数字 1-9:对每个数字 num,检查 rowcolbox 是否允许填入(即对应位置为 false)。
  3. 递归与回溯
    • 若允许,填入 num 并更新标记数组,递归处理下一个空单元格。
    • 若递归返回 true,说明找到解,直接返回 true
    • 若递归返回 false,回溯(恢复棋盘和标记数组),尝试下一个数字。
  4. 终止条件:遍历完所有单元格(无空单元格),返回 true(找到解)。
回溯函数代码
private boolean backtrack(char[][] board) {
    for (int i = 0; i < 9; i++) {
        for (int j = 0; j < 9; j++) {
            if (board[i][j] == '.') { // 找到空单元格
                for (int num = 1; num <= 9; num++) {
                    int boxIndex = (i / 3) * 3 + (j / 3);
                    // 检查行、列、宫格是否允许填入num
                    if (!row[i][num] && !col[j][num] && !box[boxIndex][num]) {
                        // 填入数字,更新标记
                        board[i][j] = (char)(num + '0');
                        row[i][num] = true;
                        col[j][num] = true;
                        box[boxIndex][num] = true;
                        
                        // 递归处理下一个单元格
                        if (backtrack(board)) {
                            return true; // 找到解,直接返回
                        }
                        
                        // 回溯:恢复状态
                        board[i][j] = '.';
                        row[i][num] = false;
                        col[j][num] = false;
                        box[boxIndex][num] = false;
                    }
                }
                return false; // 所有数字都尝试过,无解(回溯)
            }
        }
    }
    return true; // 所有单元格填满,找到解
}
步骤 3:调用回溯函数

solveSudoku 方法中初始化标记数组,然后启动回溯:

public void solveSudoku(char[][] board) {
    // 初始化标记数组(类成员变量,避免参数传递)
    row = new boolean[9][10];
    col = new boolean[9][10];
    box = new boolean[9][10];
    
    // 标记已有数字
    for (int i = 0; i < 9; i++) {
        for (int j = 0; j < 9; j++) {
            if (board[i][j] != '.') {
                int num = board[i][j] - '0';
                int boxIndex = (i / 3) * 3 + (j / 3);
                row[i][num] = true;
                col[j][num] = true;
                box[boxIndex][num] = true;
            }
        }
    }
    
    backtrack(board); // 启动回溯
}

关键逻辑解析

1. 宫格索引计算

boxIndex = (i / 3) * 3 + (j / 3)

  • i/3j/3 分别将行、列划分为 0-2 的区间(对应宫格的行、列块)。
  • 例如,i=4, j=5i/3=1, j/3=1boxIndex=1*3+1=4(第 5 个宫格,索引从 0 开始)。
2. 标记数组的作用

通过 O(1) 时间判断数字是否冲突,避免每次检查行、列、宫格时的 O(9) 遍历,大幅提升效率。

3. 回溯的核心
  • 递归深入:填入合法数字后,递归处理下一个空单元格,探索解空间。
  • 回溯恢复:若递归失败,恢复棋盘和标记数组,尝试其他数字,确保不影响上层递归的状态。

完整代码(Java)

class Solution {
    // 标记数组:row[i][num]、col[j][num]、box[boxIndex][num]
    private boolean[][] row = new boolean[9][10];
    private boolean[][] col = new boolean[9][10];
    private boolean[][] box = new boolean[9][10];

    public void solveSudoku(char[][] board) {
        // 初始化标记数组
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                if (board[i][j] != '.') {
                    int num = board[i][j] - '0';
                    int boxIndex = (i / 3) * 3 + (j / 3);
                    row[i][num] = true;
                    col[j][num] = true;
                    box[boxIndex][num] = true;
                }
            }
        }
        // 回溯填充
        backtrack(board);
    }

    private boolean backtrack(char[][] board) {
        // 遍历所有单元格,寻找空位置
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                if (board[i][j] == '.') {
                    // 尝试填入1-9
                    for (int num = 1; num <= 9; num++) {
                        int boxIndex = (i / 3) * 3 + (j / 3);
                        // 检查行、列、宫格是否允许填入num
                        if (!row[i][num] && !col[j][num] && !box[boxIndex][num]) {
                            // 填入数字,更新标记
                            board[i][j] = (char)(num + '0');
                            row[i][num] = true;
                            col[j][num] = true;
                            box[boxIndex][num] = true;

                            // 递归处理下一个位置,若找到解则返回true
                            if (backtrack(board)) {
                                return true;
                            }

                            // 回溯:恢复状态
                            board[i][j] = '.';
                            row[i][num] = false;
                            col[j][num] = false;
                            box[boxIndex][num] = false;
                        }
                    }
                    // 所有数字都尝试过,无法填充,返回false(回溯)
                    return false;
                }
            }
        }
        // 所有单元格填满,返回true(找到解)
        return true;
    }
}

示例验证(以示例1为例)

输入棋盘的空单元格逐步被填充:

  1. 找到第一个空单元格,尝试合法数字(如 (0,2) 位置填入 4,需检查行、列、宫格无冲突)。
  2. 递归处理下一个空单元格,持续填充直到所有单元格填满。
  3. 由于题目保证唯一解,回溯过程最终会找到合法填充方式。

该方法通过 回溯 + 标记数组,高效约束解空间,确保在合理时间内找到数独的唯一解,是处理此类约束满足问题的经典方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值