【递归算法】解数独

文章摘要:

  • 这篇文章详细讲解了如何解决数独问题。主要内容包括: 数独规则分析:每行、每列和每个3x3宫格内数字1-9不能重复 使用三个布尔数组(行、列、宫格)来记录数字出现情况 基于回溯算法的解决思路: 通过DFS遍历每个空格 尝试填入1-9的数字,检查合法性 合法则递归继续填下一个空格 不合法则回溯并尝试其他数字 提供了完整的Java代码实现,包括初始化状态记录和DFS解法 文章强调先完成"有效的数独"作为前置题目,并给出了详细的算法解析和代码注释,帮助理解回溯法在解决数独问题中的应用。

文章目录

37. 解数独

一、题目解析

我们需要解决题目给出的数独问题,在解决数独的同时需要注意数独的合法条件:

  1. 每一行只能出现一次数字 1~9
  2. 每一列只能出现一次数字 1~9
  3. 每一个 3 x 3 九宫格内只能出现一次数字 1~9

如图:

二、算法原理 + 代码实现

前置题目

做本题之前,可以先去做一下本题的前置题目:36. 有效的数独,这道题目中只需要考虑数独的合法条件如何编写,而这个条件正是本题剪枝所需要的。

在前置题目中,我们只需要判断题目给出的数独是否是有效的数独即可,那么就抓住三个:行、列以及九宫格。

如何判断数独中的某一个位置所在的行、列以及九宫格中是否存在相同的数字呢?

我们可以使用哈希的策略:定义三个布尔类型的变量(数组),分别记录行、列以及九宫格中是否存在某个数字。

使用二维数组 checkRow[9][10] 和 checkCol[9][10] 分别记录当前位置所在的行和列中是否存在某个数字(第一个括号用来表示行数/列数,第二个括号用来表示数字 1~9)。

我们可以将整个数独以九宫格为单位 来划分:

这样就可以使用一个二维数组来表示每一个九宫格了,于是使用一个三维数组表示九宫格: grid[3][3][10] 记录某一个九宫格中是否存在某个数字(第一第二个括号用来表示哪一个九宫格,第三个括号表示数字 1~9)。

通过这三个布尔数组我们就可以快速地对数独进行判断了,大概思路:

  1. 对数独进行遍历,拿到数独中的数字
  2. 对数字所处的位置的行、列及九宫格进行合法性判断(利用三个数组):若不合法就返回false;若合法就将更改数组的映射状态为true,表示当前位置已经存在数字

代码实现如下:

Java 复制代码
class Solution {
    public boolean isValidSudoku(char[][] board) {
        boolean[][] row = new boolean[9][10];
        boolean[][] col = new boolean[9][10];
        boolean[][][] grid = new boolean[3][3][10];

        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                char ch = board[i][j];
                if (ch >= '1' && ch <= '9') {
                    // 判断是否有效
                    int num = ch - '0'; // 数字字符可以直接减'0'
                    if (row[i][num] || col[j][num] || grid[i/3][j/3][num]) {
                        // 都不满足,返回false
                        return false;
                    }
                    row[i][num] = col[j][num] = grid[i/3][j/3][num] = true;
                }
            }
        }
        return true;
    }
}

决策树

做过前置题目后,我们做这道题目就方便很多了。

我们从上往下依次遍历每一行,然后在一行中依次遍历每一列,看看能将 1~9 中的哪个数填进去(依据有效的数独那题的核心思路来判断),如果可以填入,就递归看看基于这个选择的后续情况。

根据这个思路,画出的部分决策树如下:

从决策树中可以看到,我们的选择在后面可能有不合法的情况,这时候要及时告知上一层并更改选择(dfs 函数来处理)。

全局变量

本题的全局变量主要是前置题目中的那些全局变量(布尔数组),由于我们直接在数组中进行修改,因此不需要返回任何值。

为了递归时更方便,我们将题目给出的数独(board)改成全局变量。

Java 复制代码
boolean[][] checkRow, checkCol; // 用于记录行和列的情况
boolean[][][] grid; // 用于记录九宫格的情况
char[][] board;

dfs 函数

函数头

本题只涉及到对数组的修改,而且我们已经提前将题目所给的数组改成全局变量,不需要将其作为参数了。

这里比较重要的是函数的返回值,这里我们需要知道填入一个数字后,基于该选择的情况是否能够解决数独,如果无法解决数独就及时更改数字(剪枝)以减少不必要的递归操作,因此将函数的返回值设置成布尔类型。

Java 复制代码
boolean dfs();

函数体

我们按照先遍历行再遍历列的顺序嵌套两层循环,然后拿出二维数组(数独)中的一个字符,判断它是否是数字,如果不是数字,就看看可以填入 1~9 中的哪个数字,填入数字之后就看看基于这个数字的情况是否是合法的,如果是合法的,就返回 true;否则就返回现场。如果数字全部无法填入,就返回 false,告诉上层这个选择不可以解决;如果遍历行和遍历列的循环都已经走完了但是还未返回,说明已经没有空位置了,此时也返回 true。

Java 复制代码
for (int row = 0; row < 9; row++) {
	for (int col = 0; col < 9; col++) {
		// 拿到二维数组某个位置的一个字符
		// 判断该字符是否是数字,若不是数字,就遍历数字 1~9 并判断三个布尔数组条件是否满足
		// 若都满足,就填入该数字
		// 然后递归
	}
}
return true;

细节问题

回溯

这里的回溯是将对应 row 和 col 位置的字符改回 .,注意也要将三个记录数独情况的布尔数组更新。

剪枝

本题的剪枝操作通过"有效的数独"这一题的核心代码实现,就是通过判断数独中某一位置的同一行、列、九宫格中的数字情况来决定是否填入数字。

递归出口

递归出口就通过 dfs 函数的返回来实现,当填入的数字已满,就已经结束递归了。

代码实现

要注意,我们解数独之前要将三个记录数独状态的布尔数组先更新一下,让它们记录下题目所给的数独的初始状态。

Java 复制代码
class Solution {
    boolean[][] checkRow, checkCol;
    boolean[][][] grid;
    char[][] board;

    public void solveSudoku(char[][] givenBoard) {
        board = givenBoard;
        checkRow = new boolean[9][10];
        checkCol = new boolean[9][10];
        grid = new boolean[3][3][10];

        // 记录数独初始状态
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                char ch = board[i][j];
                if (ch >= '1' && ch <= '9') {
                    int num = ch - '0';
                    checkRow[i][num] = checkCol[j][num] = grid[i/3][j/3][num] = true;
                }
            }
        }

        dfs();
    }

    private boolean dfs() {
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                char ch = board[i][j];
                if (ch == '.') {
                    // 将数字放到该位置上
                    for (int num = 1; num <= 9; num++) {
                        if (!checkRow[i][num] && !checkCol[j][num] && !grid[i/3][j/3][num]) {
                            board[i][j] = (char)(num + '0');
                            checkRow[i][num] = checkCol[j][num] = grid[i/3][j/3][num] = true;
                            if (dfs()) return true; // 告诉上层该位置放该数字有效
                            // 若放法无效,就恢复现场
                            board[i][j] = '.';
                            checkRow[i][num] = checkCol[j][num] = grid[i/3][j/3][num] = false;
                        }
                    }
                    return false; // 若走到此处仍未返回,说明该位置已经没有数字能放了
                }
            }
        }
        return true; // 若走到此处仍未返回,说明数独中已经没有空位置
    }
}

文章到这里就结束啦,若有错误请尽管指出~

完

相关推荐
alphaTao20 小时前
LeetCode 每日一题 2026/10/19-2026/10/25
算法·leetcode
木头科技20 小时前
【AI 工程化第十六篇】Spring AI 多模态 RAG 实战:图片、扫描件、表格与图文证据链
java·人工智能·spring
驭渊的小故事21 小时前
从 JVM 底层看泛型:泛型擦除、Signature 属性与 Super Type Token 模式
java·开发语言·jvm
菜鸟小杰子21 小时前
手动修改 Spring Boot 嵌套 JAR 包:大包小包隔离打包实战
java·jar
计算机编程-吉哥21 小时前
计算机毕业设计 基于 SpringBoot + Vue 的剧本杀系统 Java毕业设计【任务书、开题报告、中期报告、论文、源码】
java·vue.js·spring boot·毕业设计·课程设计·计算机毕业设计选题
code2cat21 小时前
【随笔】RAG语义缓存:问题很相似,答案就能直接复用吗
java·缓存·知识库·rag
Rain的Java大神之路21 小时前
面了 50 个候选人,90% 都挂在 LeetCode 3 无重复字符的最长子串:滑动窗口的 5 个致命坑
java·后端·leetcode
西凉的悲伤21 小时前
Jsoup 库 HTML 解析详解
java·前端·html·jsoup
十碗饭吃不饱21 小时前
Apache POI 踩坑:setFontFamily 二次设置字体不生效、样式残留问题深度解析
java·word·apache
我命由我123451 天前
6-2.Python 异常处理 - 异常捕获(捕获指定异常、常见异常类型、异常信息)
java·运维·开发语言·python·http·https·java-ee