当前位置: 首页 > news >正文

【LeetCode 每日一题】37. 解数独

Problem: 37. 解数独

文章目录

  • 整体思路
  • 完整代码
  • 时空复杂度
    • 时间复杂度:O(1) 或 O(9^E)
    • 空间复杂度:O(1) 或 O(E)

整体思路

这段代码的目的是用一个高效的回溯算法(Backtracking)来解决一个 9x9 的数独谜题。它不仅实现了基本的回溯,还引入了一个重要的性能优化启发式策略:最少剩余价值(Minimum Remaining Values, MRV),也称为 最受约束优先 (Most Constrained First)

算法的整体思路可以分解为以下几个核心阶段:

  1. 预处理与状态初始化

    • 状态记录:创建了三个布尔数组 row, col, area 来实时跟踪每一行、每一列、每一个 3x3 子区域中哪些数字已经被使用。这使得检查一个数字在某个位置是否合法的时间复杂度降低到 O(1)。
    • 收集空位:遍历一次初始棋盘,完成两件事:
      a. 用预设的数字填充 row, col, area 状态。
      b. 将所有待填充的空单元格(值为 .)的坐标 (i, j) 收集到一个列表 emptyPos 中。
  2. 应用MRV启发式策略

    • 这是该算法与朴素回溯算法最大的区别和优势所在。它不按固定的顺序(如从左到右,从上到下)填充空格,而是优先填充选择最少的空格。
    • 候选数计算:遍历 emptyPos 列表中的每个空位,调用 getCandidates 函数计算出该位置有多少个合法的数字可以填(即候选数)。
    • 优先队列:将每个空位的信息打包成 [候选数, 行索引, 列索引] 的形式,并存入一个最小优先队列 (Min-Priority Queue) emptyPQ 中。这样,队列的顶部永远是候选数最少的那个空格。
    • 为什么这样做? 优先处理选择最少的空格,可以让我们更早地发现错误的路径(如果一个位置一个候选数都没有,那么之前的选择肯定是错的),从而进行更早地“剪枝”,极大地减少了搜索空间。
  3. 深度优先搜索 (DFS) / 回溯

    • dfs 函数是回溯算法的核心。
    • 基准情形 (Base Case):如果优先队列 emptyPQ 为空,说明所有的空格都已经被成功填充,数独已解,返回 true
    • 递归步骤
      a. 选择:从优先队列中取出 poll() 一个“最受约束”的空格 (i, j)
      b. 遍历候选:对数字 1 到 9 进行遍历,检查每个数字是否在 (i, j) 位置合法(通过查询 row, col, area 状态)。
      c. 尝试 (Act):如果一个数字 x 合法:
      * 将 board[i][j] 设置为该数字。
      * 更新 row, col, area 状态,标记该数字已被使用。
      * 进行递归调用 dfs(...),尝试解决剩下的谜题。
      d. 判断与回溯 (Undo)
      * 如果递归调用返回 true,说明找到了一个解,直接将 true 向上层返回,终止搜索。
      * 如果递归调用返回 false,说明基于当前数字 x 的尝试是死胡同。必须进行回溯:撤销刚才的尝试,将 row, col, area 状态恢复到放置 x 之前的状态(设置为 false)。然后继续尝试下一个合法的候选数字。
      e. 恢复状态:如果对当前空格 (i, j) 的所有候选数字都尝试失败了,说明上一步的选择有误。此时需要将取出的空格信息重新放回优先队列(这是为了恢复队列状态,以便上层递归函数能正确运行),然后返回 false

完整代码

import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;class Solution {/*** 使用带MRV优化的回溯算法解决数独问题。* @param board 9x9 的数独棋盘*/public void solveSudoku(char[][] board) {// 状态记录数组boolean[][] row = new boolean[9][9]; // row[i][x] 表示第i行是否已使用数字x+1boolean[][] col = new boolean[9][9]; // col[j][x] 表示第j列是否已使用数字x+1boolean[][][] area = new boolean[3][3][9]; // area[i/3][j/3][x] 表示对应3x3区域是否已使用数字x+1// 存储所有空位的坐标List<int[]> emptyPos = new ArrayList<>();// 步骤1: 预处理棋盘,初始化状态并收集空位for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {if (board[i][j] == '.') {emptyPos.add(new int[]{i, j});} else {int x = board[i][j] - '1';row[i][x] = col[j][x] = area[i / 3][j / 3][x] = true;}}}// 步骤2: 应用MRV启发式策略,创建优先队列// 队列按“候选数”升序排列,候选数越少,优先级越高PriorityQueue<int[]> emptyPQ = new PriorityQueue<>((a, b) -> a[0] - b[0]);for (int[] pos : emptyPos) {int i = pos[0];int j = pos[1];// 计算每个空位的候选数int candidates = getCandidates(i, j, row, col, area);// 将 [候选数, 行, 列] 存入优先队列emptyPQ.offer(new int[]{candidates, i, j});}// 步骤3: 开始回溯搜索dfs(board, row, col, area, emptyPQ);}/*** 计算 (i, j) 位置有多少个合法的候选数字。*/private int getCandidates(int i, int j, boolean[][] row, boolean[][] col, boolean[][][] area) {int candidates = 9;for (int x = 0; x < 9; x++) {if (row[i][x] || col[j][x] || area[i / 3][j / 3][x]) {candidates--;}}return candidates;}/*** 核心回溯函数*/private boolean dfs(char[][] board, boolean[][] row, boolean[][] col, boolean[][][] area, PriorityQueue<int[]> emptyPQ) {// 基准情形:如果优先队列为空,说明所有空位都已填满,找到解if (emptyPQ.isEmpty()) {return true;}// 选择:从队列中取出候选数最少的空位int[] top = emptyPQ.poll();int i = top[1];int j = top[2];// 用于记录失败尝试的次数,以便恢复状态时使用。int failedAttempts = 0;// 遍历所有可能的数字 (1-9)for (int x = 0; x < 9; x++) {// 检查数字 x+1 是否合法if (row[i][x] || col[j][x] || area[i / 3][j / 3][x]) {continue;}// 尝试:放置数字并更新状态board[i][j] = (char) ('1' + x);row[i][x] = col[j][x] = area[i / 3][j / 3][x] = true;// 递归:解决剩下的谜题if (dfs(board, row, col, area, emptyPQ)) {return true; // 成功找到解,直接返回}// 回溯:撤销尝试,恢复状态row[i][x] = col[j][x] = area[i / 3][j / 3][x] = false;// board[i][j] 不需要恢复为'.', 因为下一次循环会覆盖它failedAttempts++;}// 如果所有数字都尝试失败,恢复优先队列状态并返回 false// **注意**: 此处的实现逻辑可能存在问题。正确的做法是直接将 `top` 重新 offer 回去。// `new int[]{candidates, i, j}` 中的 `candidates` 是一个本地变量,其值不反映真实的候选数。// 但为了忠于原代码分析,我们保留此实现。emptyPQ.offer(new int[]{top[0], i, j}); // 应该 offer(top)return false;}
}

时空复杂度

时间复杂度:O(1) 或 O(9^E)

  1. 理论分析
    • 对于一个通用的 N x N 数独,如果有 E 个空格,每个空格最多有 N^2 个选择,所以一个非常宽松的上限是 O(N2E)。
    • 对于 9x9 数独,E 是空单元格的数量(最多为81)。在最坏的情况下,每个空位都有9个选择,所以理论上的时间复杂度是 O(9^E)
  2. 实际分析
    • 由于数独的板子大小是固定的 9x9,E 的最大值也是一个常数 (81)。因此,从严格的算法复杂度理论来看,解决一个固定大小问题的操作数是一个巨大的常数。所以时间复杂度可以认为是 O(1)
    • O(9^E) 更好地描述了算法的搜索行为,而 O(1) 描述了它在固定问题规模下的性能。
    • MRV启发式策略会极大地剪枝,使得实际运行时间远小于理论上界。
  3. 预处理部分:扫描棋盘和建立优先队列的时间是 O(81) + O(E log E),其中 E <= 81,这部分是常数时间,可以忽略不计。

空间复杂度:O(1) 或 O(E)

  1. 状态数组row, col, area 数组的大小都是固定的(9x9, 9x9, 3x3x9),总共占用 81 + 81 + 81 = 243 个布尔值的空间。这是 O(1) 的。
  2. 数据结构emptyPosemptyPQ 最多存储 E 个元素,其中 E <= 81。这也是 O(E)O(1) 的。
  3. 递归栈深度:回溯算法的递归深度在最坏情况下等于需要填充的空格数 E。因此,调用栈会使用 O(E) 的空间。

综合分析
空间开销主要由递归栈深度决定。因此,空间复杂度为 O(E),其中 E 是空单元格的数量。同样,由于 E 有一个固定的上限81,所以严格来说空间复杂度也是 O(1)

参考灵神


文章转载自:

http://7oyxFxys.tqsgt.cn
http://rJZWuArK.tqsgt.cn
http://p5wB42Mr.tqsgt.cn
http://n0BoZ3H7.tqsgt.cn
http://ietOXj9z.tqsgt.cn
http://DAPOvQ0w.tqsgt.cn
http://xiWIDuOE.tqsgt.cn
http://GABLqY8R.tqsgt.cn
http://L9fKaZbS.tqsgt.cn
http://l2KWHVhZ.tqsgt.cn
http://t4gqSEuq.tqsgt.cn
http://1c6njGq0.tqsgt.cn
http://lPzmmZqh.tqsgt.cn
http://U7O0t5jn.tqsgt.cn
http://sT3wkH3b.tqsgt.cn
http://tcC8GIWq.tqsgt.cn
http://slrnHliM.tqsgt.cn
http://wwcAWwFj.tqsgt.cn
http://VwXrjQTH.tqsgt.cn
http://zZJZCDW5.tqsgt.cn
http://iy8nGoYf.tqsgt.cn
http://63kTx0NS.tqsgt.cn
http://VyuOEnGL.tqsgt.cn
http://lv6miKSs.tqsgt.cn
http://sVLDCR0T.tqsgt.cn
http://Xd0yd9ML.tqsgt.cn
http://s28lnAqU.tqsgt.cn
http://UnR8GHjt.tqsgt.cn
http://qObBhu6W.tqsgt.cn
http://OjCwKt6k.tqsgt.cn
http://www.dtcms.com/a/385517.html

相关文章:

  • 多项式回归:线性回归的扩展
  • AI生成到无缝PBR材质:Firefly+第三方AI+Substance工作流
  • Java分布式锁实战指南:从理论到实践
  • 【CSS】层叠上下文和z-index
  • inline-block元素错位原因及解决方法
  • 【Java】P3 Java基础:关键字、标识符与变量详解
  • Golang语言入门篇003_Go源代码结构
  • 【Docker】报错Data page checksums are disabled.
  • Viper:Go语言中强大的配置管理库入门教程
  • ISO/PAS 5112 附录A 与21434 WPs的映射关系
  • 机器学习-Bagging
  • OpenCV 图像拼接实战:从特征检测到全景融合
  • Atlas-Chain:一个灵活的Java责任链框架设计与实现
  • FBX/OBJ/MAX/GLB/GLTF怎么处理成3dtiles,制作B3DM格式模型文件
  • 金融数据---获取问财数据
  • Python(1)|| 超基础语法(格式,输入输出,变量,字符串,运算符)
  • Linux 文本处理三剑客:grep、sed 与 awk
  • docker-webtop+cpolar:无感远程Linux桌面方案
  • 随机森林模型:基于天气数据集的分类任务全流程解析
  • Linux vim快捷键记录
  • 聊聊大模型的self-training:从Test-time RL说起
  • 星穹无损合约:以信任为基石,开启DeFi新纪元
  • cJSON的安装和使用
  • godot+c#实现玩家的简单移动
  • 【工具】多线程任务执行函数
  • 使用 Spring Boot 搭建和部署 Kafka 消息队列系统
  • scikit-learn pipeline做数据预处理 模板参考
  • MATLAB的二维SIMPLE算法实现方腔自然对流
  • SPMI总线协议(二)
  • 全场景流畅投屏,跨 VLAN 技术成酒店智能升级核心动力