目录
[一、单词搜索(LeetCode 79)](#一、单词搜索(LeetCode 79))
[Java 代码实现](#Java 代码实现)
[易错点 & 优化点](#易错点 & 优化点)
[二、分割回文串(LeetCode 131)](#二、分割回文串(LeetCode 131))
[Java 代码实现](#Java 代码实现)
[易错点 & 优化点](#易错点 & 优化点)
一、单词搜索(LeetCode 79)
题目描述
给定一个 m x n 二维字符网格 board 和一个字符串单词 word。如果 word 存在于网格中,返回 true;否则,返回 false。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中 "相邻" 单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
plaintext
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
解题思路
这道题的核心是DFS + 回溯,本质是在二维网格中进行深度优先搜索,寻找符合条件的路径:
- 遍历起点:遍历网格中的每个单元格,若单元格字符与单词首字符相同,则以该单元格为起点开始 DFS;
- DFS 过程:从当前单元格出发,向上下左右四个方向搜索,匹配单词的下一个字符;
- 标记访问 :为避免重复使用单元格,在访问时将单元格临时标记为
'#'(或其他特殊字符),回溯时恢复; - 终止条件 :
- 若当前匹配的字符索引等于单词长度,说明已找到完整单词,返回
true; - 若单元格越界、字符不匹配或已被访问,返回
false。
- 若当前匹配的字符索引等于单词长度,说明已找到完整单词,返回
Java 代码实现
java
运行
public class WordSearch {
public boolean exist(char[][] board, String word) {
int rows = board.length;
int cols = board[0].length;
// 遍历网格中的每个单元格作为起点
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
// 若起点字符与单词首字符相同,开始DFS
if (board[i][j] == word.charAt(0) && dfs(board, i, j, word, 0)) {
return true;
}
}
}
return false;
}
/**
* DFS搜索单词路径
* @param board 二维网格
* @param i 当前行索引
* @param j 当前列索引
* @param word 目标单词
* @param index 当前匹配的单词字符索引
* @return 是否找到单词路径
*/
private boolean dfs(char[][] board, int i, int j, String word, int index) {
// 终止条件:匹配到单词末尾,返回true
if (index == word.length()) {
return true;
}
// 终止条件:越界、字符不匹配或已被访问,返回false
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length
|| board[i][j] != word.charAt(index)) {
return false;
}
// 标记当前单元格为已访问(临时修改为特殊字符)
char temp = board[i][j];
board[i][j] = '#';
// 向上下左右四个方向递归搜索
boolean found = dfs(board, i + 1, j, word, index + 1)
|| dfs(board, i - 1, j, word, index + 1)
|| dfs(board, i, j + 1, word, index + 1)
|| dfs(board, i, j - 1, word, index + 1);
// 回溯:恢复当前单元格的字符
board[i][j] = temp;
return found;
}
}
复杂度分析
- 时间复杂度:O(m×n×4L),其中m和n是网格的行数和列数,L是单词长度。每个单元格作为起点,最多有 4 个方向的分支,递归深度为L;
- 空间复杂度:O(L),递归栈的深度最多为单词长度L。
易错点 & 优化点
- 访问标记:必须临时修改单元格字符标记访问,回溯时恢复,否则会影响后续搜索;
- 剪枝优化 :若当前字符不匹配,直接返回
false,无需继续搜索; - 提前终止 :找到完整单词后,直接返回
true,避免不必要的搜索。
二、分割回文串(LeetCode 131)
题目描述
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。回文串是正着读和反着读都一样的字符串。
示例:
plaintext
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
解题思路
这道题的核心是回溯 + 预处理回文判断,通过枚举分割点,生成所有可能的回文分割方案:
- 预处理回文 :先用动态规划预处理字符串,记录任意子串
s[i..j]是否为回文串,避免每次递归都判断回文,提升效率; - 回溯枚举分割点 :从字符串
s的起始位置出发,枚举每个位置作为分割点,判断子串s[start..i]是否为回文; - 递归与回溯:若子串是回文,将其加入当前路径,递归处理剩余子串;递归结束后,回溯移除路径中的子串;
- 终止条件:当分割点到达字符串末尾时,将当前路径加入结果集。
Java 代码实现
java
运行
import java.util.ArrayList;
import java.util.List;
public class PartitionPalindrome {
List<List<String>> result = new ArrayList<>();
List<String> path = new ArrayList<>();
boolean[][] isPalindrome; // 预处理回文数组
public List<List<String>> partition(String s) {
int n = s.length();
// 预处理:判断所有子串是否为回文
isPalindrome = new boolean[n][n];
preprocessPalindrome(s, n);
// 回溯枚举分割点
backtrack(s, 0);
return result;
}
/**
* 预处理回文子串
* @param s 原字符串
* @param n 字符串长度
*/
private void preprocessPalindrome(String s, int n) {
// 单个字符都是回文
for (int i = 0; i < n; i++) {
isPalindrome[i][i] = true;
}
// 长度为2的子串
for (int i = 0; i < n - 1; i++) {
isPalindrome[i][i + 1] = (s.charAt(i) == s.charAt(i + 1));
}
// 长度大于2的子串
for (int len = 3; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
isPalindrome[i][j] = (s.charAt(i) == s.charAt(j)) && isPalindrome[i + 1][j - 1];
}
}
}
/**
* 回溯枚举分割点
* @param s 原字符串
* @param start 当前分割的起始位置
*/
private void backtrack(String s, int start) {
// 终止条件:分割点到达字符串末尾,保存当前路径
if (start == s.length()) {
result.add(new ArrayList<>(path));
return;
}
// 枚举分割点,从start到s.length()-1
for (int i = start; i < s.length(); i++) {
// 若子串s[start..i]是回文串
if (isPalindrome[start][i]) {
// 做选择:将回文子串加入路径
path.add(s.substring(start, i + 1));
// 递归处理剩余子串(分割点更新为i+1)
backtrack(s, i + 1);
// 回溯:移除路径中的最后一个子串
path.remove(path.size() - 1);
}
}
}
}
复杂度分析
- 时间复杂度:O(n×2n),其中n是字符串长度。预处理回文的时间为O(n2),回溯过程中,字符串有2n−1种分割方式,每种方式需要O(n)时间生成子串;
- 空间复杂度:O(n2+n),预处理回文数组占用O(n2)空间,递归栈深度和路径存储的最大长度为O(n)。
易错点 & 优化点
- 预处理回文:动态规划预处理可将回文判断的时间复杂度从O(n)降至O(1),大幅提升整体效率;
- 分割点枚举 :必须从
start开始枚举分割点,避免重复分割; - 路径拷贝 :将路径加入结果集时,必须创建新的
ArrayList,否则后续修改会影响结果。
三、回溯法进阶技巧总结
这两道题让我们看到了回溯法的更多应用场景,也掌握了两个进阶技巧:
- 结合 DFS:在二维网格、图等结构中,通过 DFS 实现回溯,标记访问状态避免重复;
- 预处理优化:对于重复判断的条件(如回文),先通过动态规划预处理,将单次判断的时间复杂度降至O(1),减少无效递归。
回溯法的核心依然是 "枚举 + 剪枝",但在不同场景下,需要结合问题特点灵活调整:
- 网格 / 图搜索:需要标记访问状态,防止循环和重复访问;
- 字符串分割:可通过预处理减少重复判断,提升效率。