LeetCode 132. 分割回文串 II
题目描述

题意分析
将非空字符串分成若干连续子串,要求每段都是回文,求最少切割次数。切成
k段只需k - 1刀;若整个字符串已经是回文,答案为 0。
解法:回文预处理 + 前缀动态规划
核心思路
[!blue]
枚举最后一段的起点,就能把一个完整划分拆成“前缀的最优划分”和“最后一个回文子串”。但若每次都重新扫描末段判断回文,总时间会达到 $O(n^3)$,因此先把所有子串的回文性保存下来。
palindrome[left][right]表示闭区间s[left..right]是否为回文。两端字符必须相同;若长度不超过 3,内部为空或只有一个字符,已经满足回文条件,否则还要求palindrome[left + 1][right - 1]为真。按右端点递增计算,内部区间的右端点更小,所依赖的状态一定已经得到;短区间通过短路判断,不会访问无效下标。再定义
dp[right]为前缀s[0..right]的最少切割次数。枚举最后一段起点left,只考虑palindrome[left][right]为真的情况:若left = 0,整个前缀本身就是回文,不用切;否则在left前面切一刀,候选答案是dp[left - 1] + 1。每种合法划分都有一个最后回文段,其起点一定会被枚举。固定这个末段后,前缀必须使用最少切割方案,否则替换为更优前缀还能减少总刀数。因此对所有候选取最小值,就能得到当前前缀的最优答案。
dp[right]初始设为right,对应把前缀的right + 1个字符分别成段,这始终是合法方案。按right从小到大转移,读取的dp[left - 1]已经算好;单字符前缀会得到 0,最终返回dp[n - 1]。
解题步骤
- 建立
n × n的回文表,按右端点递增枚举区间。- 对每个右端点
i,先把dp[i]初始化为最坏情况i次切割。- 枚举最后一段起点
j;若palindrome[j][i]为真,用 0 或dp[j-1] + 1更新。- 返回
dp[n-1]。
代码实现
class Solution {
public int minCut(String s) {
int n = s.length();
boolean[][] palindrome = new boolean[n][n];
// 右端点递增,内部回文状态已计算;短区间通过两端相等直接判断。
for (int right = 0; right < n; right++) {
for (int left = 0; left <= right; left++) {
palindrome[left][right] =
s.charAt(left) == s.charAt(right)
&& (right - left <= 2 || palindrome[left + 1][right - 1]);
}
}
int[] dp = new int[n];
for (int right = 0; right < n; right++) {
// 每个字符单独一段时需要当前右下标这么多刀,作为最坏初值。
dp[right] = right;
for (int left = 0; left <= right; left++) {
if (!palindrome[left][right]) {
continue;
}
// 整段前缀为回文时无需前一刀,否则从更短前缀加一刀转移。
dp[right] = left == 0 ? 0 : Math.min(dp[right], dp[left - 1] + 1);
}
}
return dp[n - 1];
}
}
func minCut(s string) int {
n := len(s)
palindrome := make([][]bool, n)
for i := range palindrome {
palindrome[i] = make([]bool, n)
}
// 右端点递增,内部回文状态已计算;短区间通过两端相等直接判断。
for right := 0; right < n; right++ {
for left := 0; left <= right; left++ {
palindrome[left][right] = s[left] == s[right] &&
(right-left <= 2 || palindrome[left+1][right-1])
}
}
dp := make([]int, n)
for right := 0; right < n; right++ {
// 每个字符单独一段时需要当前右下标这么多刀,作为最坏初值。
dp[right] = right
for left := 0; left <= right; left++ {
if !palindrome[left][right] {
continue
}
// 整段前缀为回文时无需前一刀,否则从更短前缀加一刀转移。
if left == 0 {
dp[right] = 0
} else if dp[left-1]+1 < dp[right] {
dp[right] = dp[left-1] + 1
}
}
}
return dp[n-1]
}
复杂度分析
- 时间复杂度:$O(n^2)$,
n为字符串长度。回文预处理和前缀动态规划都枚举全部区间,每次判断、转移均为 $O(1)$。- 空间复杂度:$O(n^2)$。二维回文表占主导,
dp为 $O(n)$。
关键点总结
[!green]
- 把「最后一段必须回文」作为切分点,能把完整方案拆成已求解前缀与一个回文后缀。
- 预处理回文性后,每次状态转移只需 $O(1)$ 判断。
dp[i]记录的是切割次数,不是回文段数量;新增一段对应新增一刀。- 整个前缀为回文时答案是 0,必须单独处理
left = 0。
易错点总结
[!yellow]
- 返回回文段数量:题目要求相邻段之间的切割次数,不能直接返回段数。
- 长度 1 或 2 的区间仍访问内部状态:会出现越界;短区间只需比较两端。
- 回文表遍历顺序错误:读取
palindrome[left+1][right-1]前必须保证它已计算。- 统一写成
dp[left-1] + 1:left = 0时会访问负下标。- 每次转移临时扫描判断回文:会把总复杂度退化到 $O(n^3)$。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 131. 分割回文串 | 中等 | 回文子串判定相同,原题输出全部划分,本题只保留最少切割的前缀状态。 |
| 647. 回文子串 | 中等 | 回文区间是切割转移的前置条件,可复用中心扩展或区间DP的判定方式。 |
| 5. 最长回文子串 | 中等 | 用区间或中心扩展刻画回文结构;本题在回文区间上递推最少切割次数,该题寻找最长连续回文。 |
| 516. 最长回文子序列 | 中等 | 用区间或中心扩展刻画回文结构;本题在回文区间上递推最少切割次数,该题允许跳过字符求最长回文子序列。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!