【递归算法】解数独

文章摘要:

  • 这篇文章详细讲解了如何解决数独问题。主要内容包括: 数独规则分析:每行、每列和每个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; // 若走到此处仍未返回,说明数独中已经没有空位置
    }
}

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

相关推荐
t***5442 小时前
如何在 Dev-C++ 中切换编译器
java·开发语言·c++
Lisonseekpan2 小时前
Git:如何将一个分支的特定提交合并到另一个分支?
java·大数据·git·后端·elasticsearch
Boop_wu2 小时前
[Java EE 进阶]Mybatis进阶(动态SQL)
java·数据库·maven·mybatis
上弦月-编程2 小时前
企业级RAG系统构建指南
leetcode
大肥羊学校懒羊羊2 小时前
完数与盈数的计算题解
数据结构·c++·算法
阿Y加油吧2 小时前
算法实战笔记:LeetCode 31 下一个排列 & 287 寻找重复数
笔记·算法·leetcode
穿条秋裤到处跑2 小时前
每日一道leetcode(2026.04.24):距离原点最远的点
算法·leetcode·职场和发展
wayz112 小时前
Day 13 编程实战:朴素贝叶斯与极端涨跌预警
人工智能·算法·机器学习
BullSmall2 小时前
Redis 双机部署 完整方案(两种架构,适配两台机器)
java·redis·架构