给定一个大小为 m×n 的字符网格 board 和一个字符串 word,判断 word 是否可以在网格中找到。leetcode
单词可以通过顺序相邻的格子中的字母来构成,相邻格子指水平或垂直相邻,同一个格子中的字母在同一次构造中不能被重复使用。leetcode
题目约束如下:leetcode
- 1 ≤ m, n ≤ 6
- 1 ≤ word.length ≤ 15
- board 和 word 只包含大小写英文字母
基本思路:从暴力到 DFS
直观想法是:从每个格子出发,尝试走出一条长度为 word.length 的路径,然后把路径上经过的字符拼成字符串,最后与 word 做比较,如果相同则返回 true。这个思路的大方向是"从每个格子 DFS 搜索",但实现时有两个容易犯的错误:
- 先走满长度再比较整体字符串,导致大量无用搜索;
- 没有正确处理"一个格子只能用一次"的约束。leetcode
更合理的做法是用 DFS + 剪枝:
- 搜索过程中,当前位置的字符必须与 word[k] 相等,否则立刻剪枝返回 false;
- 匹配到 word 的最后一个字符时即可返回 true,无需再走多余的步数。leetcode
核心算法:递归 DFS + 回溯
整体框架可以概括为:外层枚举起点,内层用 DFS 向四个方向扩展。典型的 DFS 函数可以设计成:
参数 (i,j,k):当前在网格坐标 (i,j),准备匹配 word[k]。
终止条件:
- 越界、当前位置已使用、字符不相等 → 直接返回 false。
- 当 k == wordLen - 1 且字符匹配 → 找到完整单词,返回 true。
递归步骤:
- 先把当前格子标记为"已访问";
- 向上、下、左、右四个方向递归搜索下一个字符 k + 1;
- 如果任意方向返回 true,则整条链路返回 true;
- 若四个方向都失败,撤销当前格子的访问标记(回溯),返回 false。leetcode
外层逻辑则是:
- 对每个格子 (i,j) 调用 DFS,初始 k = 0;
- 只要有一次 DFS 返回 true,就可以立即返回 true;
- 如果所有起点都失败,返回 false。leetcode
visited 为什么要"回溯"
很多同学会有疑惑:图遍历题里也常用 DFS 和 visited,为什么平时好像不需要"回溯",而这题却必须恢复 visited?
关键在于:visited 的意义不同。
普通图遍历(如 Number of Islands 等)
- 目标:把一个连通块内的所有节点都访问一遍。
- visited 的含义:这个点在"整次搜索"里已经被访问过,不需要也不允许再次访问。
- 特点:visited 是一个"全局状态",一旦置为 true 就不会再变回 false,自然没有回溯的过程。leetcode
Word Search 这类"路径/组合搜索题"
- 目标:找到一条符合条件的路径,路径是有顺序的。
- visited 的含义:这个点在"当前这条路径"中已经使用过,不能在这条路径里重复使用,但换一条路径时可以重新使用。
- 特点:visited 是"依赖于当前递归路径的状态",每深入一层多一个点被使用,每回退一层就要把这个点从当前路径的使用集合中移除,这就是"回溯"。leetcode
可以用一句话概括:
- 遍历类 DFS:visited 表示"永久访问过",无需回退。
- 回溯类 DFS:visited 表示"当前候选解里用过",随递归深度变化,需要回退。
Word Search 恰好属于第二类,所以需要"标记 → 递归 → 恢复"这一整套回溯流程。leetcode
复杂度与常见剪枝
在最坏情况下,如果不做任何剪枝,复杂度大致为:
- 起点有 m⋅n 个;
- 每一步最多 4 个方向;
- 单词长度为 L;
- 于是复杂度近似为 O(m⋅n⋅4^L)。leetcode
在当前题目给定的约束下(m,n≤6,L≤15),这种 DFS + 回溯是可以接受的,但仍可以做一些剪枝优化:
- 字符统计剪枝:预先统计棋盘中每个字符的出现次数,如果 word 某个字符的出现次数大于棋盘中的次数,可以直接返回 false。
- 搜索顺序剪枝:先从和 word[0] 相等的格子启动 DFS,可以少一些无效起点。
- 搜索时一旦字符不匹配 / 越界 / 已访问,立即返回 false,避免无用递归。leetcode
一个可行的 C 实现思路说明
以 C 为例,可以按如下结构组织代码(只做思路说明,不贴完整源码):leetcode
- 定义一个 visited 二维数组,和 board 的大小一致,用 0/1 表示未访问/已访问。
- exist 函数中:
- 动态分配 visited;
- 双重循环枚举起点 (i, j);
- 对每个起点调用 dfs_visitor(board, boardSize, boardColSize, word, wordLen, 0, visited, i, j);
- 一旦某次 DFS 返回 true,就可以记录结果并跳出循环;
- 最后释放 visited,返回结果。
- dfs_visitor 中:
- 先处理终止条件:索引越界、超出边界、已访问、字符不等等直接返回 false;
- 如果 word_index == wordLen,说明前面所有字符已成功匹配,可返回 true;
- 否则标记当前格子为已访问,向四个方向递归尝试 word_index + 1;
- 任意方向成功则返回 true;
- 四个方向都失败时,把当前格子恢复成未访问状态,然后返回 false。leetcode
这种实现就完整体现了"逐字符匹配 + visited + 回溯"的思想。
总结:什么时候要回溯
整理一下经验规律:
- 如果题目只是"遍历整张图/树一次",例如求连通块、统计数量,通常只需要一个全局的 visited,不需要恢复;
- 如果题目是在"从众多可能路径/组合里找出一个/多个解",例如 Word Search、组合/排列、N 皇后等,就属于回溯问题,需要在递归时对"当前选择"做标记,在返回时撤销标记,确保其它路径可以重新使用这些节点。
Word Search 是一个非常经典的"DFS + 回溯"入门题,弄清楚这个题中 visited 和回溯的作用,对后面做各类回溯题都会有很大帮助。