问题描述
给定一个字符串 s,要求将其分割成若干子串,使得每个子串都是回文串。返回所有可能的分割方案。
示例:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
初始思路:回溯法实现
问题分析
面对分割回文串的问题,首先需要解决两个核心问题:
- 如何判断子串是回文?
- 如何高效生成所有可能的分割方案?
对于第一个问题,常规解法是使用双指针法进行回文判断。第二个问题则需要通过回溯法遍历所有可能的分割方式。
回溯法实现
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
dfs(res, new ArrayList<>(), 0, 1, s);
return res;
}
private void dfs(List<List<String>> res, List<String> holder,
int curr, int step, String s) {
if (curr + step > s.length()) {
if (step == 1) { // 成功分割标志
res.add(new ArrayList<>(holder));
}
return;
}
String candidate = s.substring(curr, curr + step);
if (isRe(candidate)) {
holder.add(candidate);
dfs(res, holder, curr + step, 1, s); // 处理下一段
holder.remove(holder.size() - 1); // 回溯
}
dfs(res, holder, curr, step + 1, s); // 扩展当前子串
}
// 双指针判断回文
private boolean isRe(String s) {
int left = 0, right = s.length() - 1;
while (left < right) {
if (s.charAt(left++) != s.charAt(right--)) {
return false;
}
}
return true;
}
}
核心逻辑解析
- 步长试探:从长度为1的子串开始尝试分割
- 回溯操作:发现有效回文子串后,固定当前分割,继续处理剩余字符串
- 分支扩展:若当前分割不成立,则尝试扩展子串长度继续判断
复杂度分析
时间复杂度
- 回文判断:单次判断时间复杂度为 O(n)
- 最坏情况:当字符串全为相同字符时,时间复杂度达 O(n² * 2ⁿ)
空间复杂度
- 递归栈深度为 O(n)
- 结果存储空间为 O(n * 2ⁿ)
负优化尝试:缓存回文判断
优化思路
通过 HashMap 缓存已判断过的子串,减少重复计算。
实现代码
class Solution {
// 添加缓存参数
public List<List<String>> partition(String s) {
// ...
Map<String, Boolean> cache = new HashMap<>();
dfs(..., cache);
return res;
}
private boolean isRe(String s, Map<String, Boolean> cache) {
if (cache.containsKey(s)) return cache.get(s);
// 判断逻辑不变,结果存入缓存
}
}
优化失败原因
- 子串数量级为 O(n²),缓存空间占用大
- 字符串操作和哈希查询引入额外开销
- 实际测试运行时间反而增加
突破性优化:动态规划预处理
优化思路
通过动态规划预处理所有可能的回文子串,将回文判断时间复杂度降至 O(1)。
动态规划实现
class Solution {
public List<List<String>> partition(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n]; // 回文状态表
// 预处理回文表(从下往上填充)
for (int i = n-1; i >= 0; i--) {
for (int j = i; j < n; j++) {
if (i == j) { // 单字符情况
dp[i][j] = true;
} else if (s.charAt(i) == s.charAt(j)) {
dp[i][j] = (j - i == 1) || dp[i+1][j-1];
}
}
}
List<List<String>> res = new ArrayList<>();
dfs(s, 0, new ArrayList<>(), res, dp);
return res;
}
private void dfs(String s, int start, List<String> path,
List<List<String>> res, boolean[][] dp) {
if (start == s.length()) {
res.add(new ArrayList<>(path));
return;
}
for (int end = start; end < s.length(); end++) {
if (dp[start][end]) {
path.add(s.substring(start, end+1));
dfs(s, end+1, path, res, dp);
path.remove(path.size()-1);
}
}
}
}
优化亮点
- 预处理时间复杂度:O(n²)
- 回溯时间复杂度:降至 O(n * 2ⁿ)
- 空间换时间:通过 O(n²) 空间换取指数级时间优化
性能对比
| 方法 | 运行时间(LeetCode测试) | 时间复杂度 |
|---|---|---|
| 基础回溯 | 9ms | O(n² * 2ⁿ) |
| 动态规划优化 | 7ms | O(n² + n*2ⁿ) |
总结与思考
- 回溯法的适用性:在问题需要遍历所有可能解时,回溯法仍然是直观有效的解决方案
- 预处理的价值:动态规划通过空间换时间的策略,显著优化了重复计算问题
- 优化陷阱:并非所有缓存策略都能奏效,需要结合具体场景分析
- 延伸思考:对于更长的输入字符串,还可以结合记忆化搜索进行进一步优化
学习建议:理解动态规划需要从基础问题入手,推荐从简单的子序列问题开始练习,逐步掌握状态转移的设计技巧。后续可以进一步研究 Manacher 算法等更高效的回文处理算法。