131. 分割回文串#
今天我们来挑战一道非常有趣的题目 - LeetCode 131「分割回文串」。这道题乍看有点复杂,但用我们小时候玩积木的思维来理解,你会发现它其实非常有意思!
🎮 妙趣横生的童年回忆#
还记得小时候玩积木吗?我们经常需要把一根长积木切分成不同的小块,每个小块都要符合某种特定的规则。今天的算法题就像是在玩一个特殊的积木游戏:我们要把一个字符串切分成若干小段,每一段都必须是回文串。
比如”aab”这个积木,我们可以这样切:
- [“a”, “a”, “b”]
- [“aa”, “b”]
是不是一下子觉得亲切了很多?
问题描述#
题目目标#
给定一个字符串 s ,请将 s 分割成一些子串,使每个子串都是回文串,并返回 s 所有可能的分割方案。
示例 1#
输入: s = "aab"
输出: [["a","a","b"],["aa","b"]]
说明: 把输入和输出放在一起看,会更容易直接抓住题目要求的变化。
💡 问题本质探索#
题目要求: 给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回所有可能的分割方案。
让我们看个具体例子:
输入:s = "aab"
输出:[["a","a","b"], ["aa","b"]]plaintext🤔 解题之前的思考#
想象我们面对一个积木(字符串),需要考虑以下几个关键问题:
-
在哪里可以切?
- 理论上每个字符之间都可以切
- 但切完的每一段都必须是回文串
-
切还是不切?
- 在每个可能的切割点,我们都面临”切”或”不切”的选择
- 这暗示了我们可能需要用回溯算法来尝试所有可能性
🚀 积木切割大法#
核心思路:回溯 + 动态规划优化#
class Solution {
// 主函数:返回字符串 s 的所有“回文分割”方案
public List<List<String>> partition(String s) {
// result:保存所有合法分割结果
List<List<String>> result = new ArrayList<>();
// dp[i][j] 表示子串 s[i..j](含 i、j)是否为回文串
// 使用该表可将回文判断从 O(len) 降为 O(1) 查询
boolean[][] dp = new boolean[s.length()][s.length()];
// 预处理所有子串的回文性质:按长度从短到长填表
for(int len = 1; len <= s.length(); len++) {
// i 为子串起点,j 为子串终点
for(int i = 0; i <= s.length() - len; i++) {
int j = i + len - 1;
// 长度为1的子串一定是回文
if(len == 1) {
dp[i][j] = true;
}
// 长度为2的子串,两个字符相等即为回文
else if(len == 2) {
dp[i][j] = s.charAt(i) == s.charAt(j);
}
// 长度大于2的子串,首尾字符相等且中间是回文串
else {
dp[i][j] = s.charAt(i) == s.charAt(j) && dp[i+1][j-1];
}
}
}
// 从下标 0 开始回溯切割,current 记录当前方案
backtrack(s, 0, new ArrayList<>(), result, dp);
// 返回全部分割方案
return result;
}
// 回溯函数:从 start 开始切割字符串,构造后续回文段
private void backtrack(String s, int start, List<String> current,
List<List<String>> result, boolean[][] dp) {
// 终止条件:start 到达字符串末尾,说明前面已切分成完整合法方案
if (start >= s.length()) {
// 加入 current 的副本,防止回溯时被修改
result.add(new ArrayList<>(current));
return;
}
// 枚举当前段的结束位置 end,尝试 s[start..end] 作为下一段
for (int end = start; end < s.length(); end++) {
// 只有当 s[start..end] 是回文时,才是有效切割
if (dp[start][end]) {
// 做选择:把当前回文子串加入方案
current.add(s.substring(start, end + 1));
// 递归处理后续剩余部分,从 end + 1 开始
backtrack(s, end + 1, current, result, dp);
// 撤销选择(回溯):移除最后一段,尝试其他 end
current.remove(current.size() - 1);
}
}
}
}java代码解释#
就像玩积木时我们会:
- 先观察每段积木是否符合要求(使用dp数组预处理所有回文子串)
- 从起点开始,尝试不同的切割位置(回溯过程)
- 记录所有符合要求的切割方案(result列表)
🎯 难点突破#
1. 回文串判断优化#
很多同学一开始都用暴力方法判断回文串,导致效率很低。使用动态规划预处理所有可能的回文子串,可以大大提高效率:
- dp[i][j] 表示 s[i..j] 是否为回文串
- 状态转移:dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1]
2. 回溯过程处理#
在每个位置,我们都面临”切”还是”不切”的选择:
- 如果切,当前子串必须是回文串
- 切完后,对剩余的字符串继续进行分割
- 记得在回溯时撤销选择
💡 优化思路#
-
空间优化:
- 可以使用滚动数组优化dp数组的空间
- 如果内存要求特别严格,可以用中心扩展法代替dp数组
-
剪枝优化:
- 如果发现剩余字符无法构成回文串,可以提前返回
- 可以记录最长回文子串长度,超过这个长度就不用尝试
🔄 相似题目推荐#
- 分割回文串 II(LeetCode 132)
- 回文子串(LeetCode 647)
- 最长回文子串(LeetCode 5)
这些题目都围绕着回文串的特性,掌握了今天的题目,相信你对这些题目也会有新的认识!