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

LeetCode算法日记 - Day 61: 解数独、单词搜索(附带模版总结)

目录

1. 解数独

1.1 题目解析

1.2 解法

1.3 题目解析

2. 单词搜索

2.1 题目解析

2.2 解法

2.3 代码实现

3. 模版总结


1. 解数独

https://leetcode.cn/problems/sudoku-solver/

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

示例 1:

输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:

提示:

  • board.length == 9
  • board[i].length == 9
  • board[i][j] 是一位数字或者 '.'
  • 题目数据 保证 输入数独仅有一个解

1.1 题目解析

题目本质
约束满足问题(CSP)。在 9×9 棋盘上给每个空格分配 1~9,使其同时满足三类不重复约束:行、列、3×3 宫。

常规解法
最直观是回溯:遇到空格就从 1~9 试,能放则继续递归,失败就回退。

问题分析
朴素回溯在 k 个空格上存在最大 9^k 的分支,若不做状态记录,会重复尝试大量非法数,搜索树爆炸。预计在“已填较少、留白多”的局面耗时明显。

思路转折
要想高效 → 必须强剪枝 → 用三张状态表做 O(1) 合法性判断:

  • 行:rowVis[9][10] 记录每行是否已用 1~9;

  • 列:colVis[9][10] 记录每列是否已用 1~9;

  • 宫:grid[3][3][10] 记录每个 3×3 宫是否已用 1~9。 预处理把已给定数字写入三表,搜索时只有“三表均未占用”的数字才尝试,回溯时对称撤销。这样可大幅缩小分支。

1.2 解法

算法思想:

  • 三表剪枝:行/列/宫三维布尔表,O(1) 判断某数字是否能放到 (r,c)。

  • 回溯搜索:找到下一个 '.' 空格,对 1..9 枚举;若三表均允许则放置→递归;失败则撤销继续试下一个。

  • 终止条件:当整盘无 '.' 时,说明已填满且合法,返回 true 一路收敛。

i)初始化:扫描棋盘,对每个已填数字 d,设置 rowVis[r][d]=colVis[c][d]=grid[r/3][c/3][d]=true。

ii)搜索入口:调用 solve(),在棋盘上顺序寻找第一个 '.'。

iii)枚举尝试:对该空格从 1..9 枚举 d:若任一 rowVis/colVis/grid 已为真则跳过;否则落子并把三表置真,递归 solve()。

iv)回溯恢复:若递归失败,恢复该格为 '.',并把对应三表位置置回 false,继续尝试下一个数字。

v)结束返回:若 1..9 全部尝试无解,返回 false 让上层回溯;若棋盘已无空格,返回 true。

易错点:

  • 字符与数字转换:放置用 (char)('0'+d),读取用 board[r][c]-'0'。

  • 宫坐标:grid[r/3][c/3][d],注意整除分组正确。

  • 回溯对称性:失败分支必须同时撤销棋盘字符与三表三处标记。

  • 布尔表下标:使用 1..9,0 位置闲置,数组长度开到 10。

  • 递归终止:当未再找到 '.' 时返回 true,勿继续搜索。

  • 搜索顺序无关性:每层从 (0,0) 开始扫描找下一个空格即可,不需要记忆上一次位置

1.3 题目解析

class Solution {boolean[][] rowVis, colVis;boolean[][][] grid;char[][] board;public void solveSudoku(char[][] _board) {rowVis = new boolean[9][10];colVis = new boolean[9][10];grid   = new boolean[3][3][10];board  = _board;// 预处理:把已填数字写入三张表for (int r = 0; r < 9; r++) {for (int c = 0; c < 9; c++) {if (board[r][c] != '.') {int d = board[r][c] - '0';rowVis[r][d] = true;colVis[c][d] = true;grid[r/3][c/3][d] = true;}}}solve(); // 回溯求解(题目保证唯一解)}// 回溯:寻找下一个空格,尝试 1..9private boolean solve() {for (int r = 0; r < 9; r++) {for (int c = 0; c < 9; c++) {if (board[r][c] == '.') {for (int d = 1; d <= 9; d++) {if (rowVis[r][d] || colVis[c][d] || grid[r/3][c/3][d]) continue;// 选择board[r][c] = (char) ('0' + d);rowVis[r][d] = colVis[c][d] = grid[r/3][c/3][d] = true;if (solve()) return true; // 向后成功,直接收敛// 撤销board[r][c] = '.';rowVis[r][d] = colVis[c][d] = grid[r/3][c/3][d] = false;}// 该空格 1..9 全失败,触发回溯return false;}}}// 未找到空格,说明已填满return true;}
}

复杂度分析

  • 时间复杂度:最坏 O(9^k),k 为空格数;由于三表强剪枝,实际远小于最坏。

  • 空间复杂度:O(k) 递归栈;三张表与棋盘大小常数级,O(1)。

2. 单词搜索

https://leetcode.cn/problems/word-search/

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例 1:

输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCCED"
输出:true

示例 2:

输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "SEE"
输出:true

示例 3:

输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCB"
输出:false

提示:

  • m == board.length
  • n = board[i].length
  • 1 <= m, n <= 6
  • 1 <= word.length <= 15
  • board 和 word 仅由大小写英文字母组成

2.1 题目解析

题目本质
在二维网格中寻找一条路径,使得路径上依次经过的格子字符恰好组成给定字符串 word,且每个格子最多使用一次。属于典型的约束路径存在性问题(CSP + DFS 回溯)。

常规解法
从所有等于首字符的格子作为起点出发,进行深度优先搜索(DFS):每一层只匹配 word[pos],若相等则向四个相邻格子尝试匹配 word[pos+1],并用访问标记 vis 保证格子不被重复使用;若某条分支失败则回溯。

问题分析
朴素暴力如果不做访问标记或不回溯,会反复走回头路、形成错误环。

思路转折
要想写对写稳:

  • 必须位置驱动:第 pos 层只匹配 word[pos](不要在同层对 word[pos..] 再做循环)。

  • 必须成对回溯:进入格子前 vis[row][col]=true,所有子分支都失败后 vis[row][col]=false。

  • 必须先判断终止:当 pos == word.length() 之前不可访问 word.charAt(pos);用“先判 pos==L 再取字符”的顺序消除越界风险。

2.2 解法

算法思想:

  • 起点:遍历全盘,凡等于 word[0] 的格子都作为起点尝试。

  • 递归:下一步要在四方邻居匹配 word[pos+1]”;当 pos == word.length() 说明整串已匹配完成。

  • 访问控制:进入某邻居前置 vis[x][y]=true,回溯失败时恢复。起点在进入 DFS 前由外层设置与恢复。

  • 若四个方向都失败,则撤销本格访问标记并返回 false。

i)读入网格尺寸,初始化 vis[m][n]。

ii)遍历每个格子 (i,j):若 board[i][j] == word[0],将其置为已访问并调用 isExist(i,j,1)。

iii)在 isExist 中:

  • 若 pos == word.length(),返回 true(整串匹配完成)。

  • 递归:取 ch = word.charAt(pos),在 4 个方向依次尝试:

    • 邻格在界内、未访问、且字符等于 ch 时,先标记 vis[x][y]=true 再递归 pos+1;

    • 若递归返回 true 立即向上返回 true;否则撤销标记继续试其它方向。

iv)若所有方向均失败,返回 false;外层起点也相应恢复访问标记并继续尝试下一个起点。

v)任一起点返回 true 即可终止整体搜索;全部失败则返回 false。

易错点:

  • 终止判断位置:本实现采用“if (pos == word.length()) return true;”在读取 word.charAt(pos) 之前,避免越界。

  • 不要在同一层对 word 再做 for(i=pos; …);这一层只匹配一个字符 word[pos],循环的是邻居。

  • 起点标记与恢复:起点在外层枚举时标记与恢复递归内不再重复标记起点自身

  • 回溯要对称:所有子分支失败后一定要把 vis[row][col] 恢复为 false。

2.3 代码实现

class Solution {boolean[][] vis;char[][] board;String word;int[] dx = {1,-1,0,0};int[] dy = {0,0,1,-1};int m,n;StringBuffer path; // 保留你的风格,未使用public boolean exist(char[][] _board, String _word) {board = _board;word  = _word;path  = new StringBuffer();m = board.length;n = board[0].length;vis = new boolean[m][n];char ch = word.charAt(0);for(int i = 0; i < m; i++){for(int j = 0; j < n; j++){if(board[i][j] == ch){vis[i][j] = true;                 // 起点进入:先标记boolean flag = isExist(i, j, 1);  // pos=1,下一步去匹配 word[1]if(flag) return true;vis[i][j] = false;                // 起点失败:恢复}}}return false;}// 从 (row,col) 开始,去邻居匹配 word[pos]public boolean isExist(int row, int col, int pos){if (pos == word.length()) return true;  // 已匹配完整串char ch = word.charAt(pos);for(int k = 0; k < 4; k++){int x = row + dx[k], y = col + dy[k];if(x>=0 && x<m && y>=0 && y<n && !vis[x][y] && board[x][y] == ch){vis[x][y] = true;                          // 进入子格:标记boolean flag = isExist(x, y, pos+1);if(flag) return true;                      // 命中直接返回vis[x][y] = false;                         // 失败:回溯}}return false; // 所有方向失败}
}

复杂度分析:

  • 时间复杂度:最坏 O(mn * 4^L),L = word.length。每个起点触发一棵深度 L、分支因子≤4 的搜索树。

  • 空间复杂度:O(L) 递归栈,外加 O(mn) 的 vis 布尔表(一次分配、反复复用)

3. 模版总结

场景一:位置驱动、按顺序遍历(顺序固定,位置推进)
特点:元素的相对顺序不改变,递归深度=位置/下标(pos/idx)。这一层不在“剩余元素里任选”,而是在当前顺序位置上决定“怎么扩展到下一位置”。
常见题:

  • 组合/子集:从 start 往后挑,不改变原顺序;

  • Word Search:pos 固定匹配 word[pos],向邻居扩展到 pos+1;

模版例如:

void dfs(int start) {collect(path);                  // 每层都是合法前缀,可收集for (int i = start; i < n; i++) {if (i > start && nums[i] == nums[i-1]) continue; // 同层去重(可选)path.add(nums[i]);dfs(i + 1);                 // 顺序推进path.remove(path.size()-1); // 回溯}
}

识别信号:

  • “保序、不换位”,或“按下标/位置一步步推进(pos→pos+1)”;

  • 本层循环多发生在动作集合(邻居/后续索引)上,而不是在“剩余元素”上任取。

场景二:选择驱动、不按顺序遍历(顺序可变,候选池里任选下一个)
特点:可以改变元素顺序;本层循环对象是候选池常用 used[] 控制是否已选。 常见题:

  • 全排列、带约束的排列(安排顺序/排座位/行程安排)。

模版例如:

void dfs() {if (path.size() == n) { collect(path); return; }for (int i = 0; i < n; i++) {if (used[i]) continue;used[i] = true;path.add(nums[i]);dfs();path.remove(path.size()-1);used[i] = false;}
}

识别信号:

  • 目标是“所有不同顺序/排列”;

  • 需要从未使用的元素中任选一个作为下一位。

  • 有重复元素时:排序 + “同层去重”条件:
    if (i>0 && nums[i]==nums[i-1] && !used[i-1]) continue;

场景三:二叉遍历(选 or 不选)
特点:对第 idx 个元素做二选一;整棵树是二叉的,叶子对应一个完整选择方案。
常见题:

  • 子集、01 决策类

模版例如:

void dfs(int idx) {if (idx == n) { collect(path); return; }// 1) 选 idxpath.add(nums[idx]);dfs(idx + 1);path.remove(path.size()-1);// 2) 不选 idxdfs(idx + 1);
}

识别信号:

  • 每个位置只有“要 / 不要”两种决策

  • 不需要在同层遍历不同候选,只对当前下标做决定。

http://www.dtcms.com/a/442360.html

相关文章:

  • 李宏毅machine learning 2021学习笔记——transformer
  • hana C# 连接问题
  • 每日一个网络知识点:TCP/IP参考模型
  • 网站报价明细网络营销战略内容
  • springboot+vue会议室管理系统(源码+文档+调试+基础修改+答疑)
  • 不依赖WMI的硬件检测工具,数据更准、速度更快
  • k8s 部署 gitlab 公网无法访问排查
  • 昆明市住房和城乡建设局网站铜川网站建设哪家好
  • 国外医院网站设计微网站建设正规公司
  • 推广网站代码中国八大设计院排名
  • 【Nest】日志记录
  • 什么网站可以做汽车国际贸易php网站开发实例教程 课件
  • [Linux基础——Lesson11.Shell运行原理------王婆传媒]
  • 梦幻创意网站建设互动平台怎么注册
  • 第三十八章 ESP32S3 SPIFFS 实验
  • Seata 与 Redisson从底层到实战
  • 如何将wsl安装的Ubuntu系统从C盘移到D盘?
  • 怎么用阿里云做网站如何开发游戏
  • 网站服务器费用免费生成ppt的网站
  • 自动驾驶中的传感器技术62——USS(8)
  • AI时代数据存储和数据恢复 | 数据恢复损坏文件修复经验建议
  • 淄博网站制作定制中国寰球工程公司
  • MTK调试- 工程模式配置
  • 黑龙江生产建设兵团知识网站商城系统平台开发
  • 做麻将网站即墨网站设计
  • 网络请求完整指南:从零开始理解前端数据交互
  • Coze源码分析-资源库-编辑知识库-前端源码-核心逻辑/API
  • 【解决】Springboot+Mybatis数据分表后前端如何根据条件映射到对应子表中查询数据?!
  • 小迪自用web笔记53
  • 芜湖做网站哪个公司好网页设计基础的课程介绍