LeetCode 132:分割回文串 II

问题本质与核心挑战
给定字符串 s
,需将其分割为若干回文子串 ,求最少分割次数。核心挑战:
- 直接枚举所有分割方式(指数级复杂度)不可行;
- 需结合 动态规划 优化分割次数计算,并通过 回文预处理 加速判断。
核心思路:动态规划 + 回文预处理
1. 回文预处理(减少重复判断)
用二维数组 isPal[i][j]
记录 子串 s[i..j]
是否为回文 ,预处理后可 O(1)
查询:
- 单个字符 :
isPal[i][i] = true
; - 两个字符 :
isPal[i][i+1] = (s[i] == s[i+1])
; - 长度 ≥3 :
isPal[i][j] = (s[i] == s[j] && isPal[i+1][j-1])
(依赖更短的子串结果)。
2. 动态规划定义与转移
- 状态定义 :
dp[i]
表示 前i
个字符(s[0..i-1]
) 的最少分割次数。 - 初始条件 :
dp[0] = -1
(空字符串的分割次数为-1
,方便后续计算);dp[i] = i-1
(最坏情况:每个字符单独分割,如"abc"
需要2
次分割)。
- 状态转移 :
对于每个j
(前j
个字符),遍历所有可能的分割点i
(0 ≤ i < j
):- 若
s[i..j-1]
是回文(isPal[i][j-1] = true
),则dp[j] = min(dp[j], dp[i] + 1)
。
- 若
算法步骤详解(以示例 s = "aab"
为例)
步骤 1:预处理回文子串(isPal
数组)
子串范围 [i,j] |
长度 | 判断逻辑 | isPal[i][j] |
---|---|---|---|
[0,0] |
1 | 单个字符 | true |
[1,1] |
1 | 单个字符 | true |
[2,2] |
1 | 单个字符 | true |
[0,1] |
2 | s[0]='a' == s[1]='a' |
true |
[1,2] |
2 | s[1]='a' ≠ s[2]='b' |
false |
[0,2] |
3 | s[0]≠s[2] (直接不满足) |
false |
步骤 2:初始化动态规划数组(dp
)
dp[0] = -1
(空字符串的分割次数);dp[1] = 0
(前1个字符"a"
,无需分割);dp[2] = 1
(初始值,后续会被更新);dp[3] = 2
(初始值,后续会被更新)。
步骤 3:状态转移计算
遍历 j
(前 j
个字符)和 i
(分割点):
j (前j 字符) |
i (分割点) |
子串 s[i..j-1] |
是否回文(isPal[i][j-1] ) |
dp[i] + 1 |
dp[j] 更新后的值 |
---|---|---|---|---|---|
j=1 |
i=0 |
"a" |
true |
-1 + 1 = 0 |
min(0, 0) = 0 |
j=2 |
i=0 |
"aa" |
true |
-1 + 1 = 0 |
min(1, 0) = 0 |
j=2 |
i=1 |
"a" |
true |
0 + 1 = 1 |
min(0, 1) = 0 |
j=3 |
i=0 |
"aab" |
false |
- | 不更新 |
j=3 |
i=1 |
"ab" |
false |
- | 不更新 |
j=3 |
i=2 |
"b" |
true |
0 + 1 = 1 |
min(2, 1) = 1 |
完整代码(Java)
java
class Solution {
public int minCut(String s) {
int n = s.length();
if (n == 0) return 0;
// 步骤1:预处理回文子串
boolean[][] isPal = new boolean[n][n];
// 处理长度为1的回文
for (int i = 0; i < n; i++) {
isPal[i][i] = true;
}
// 处理长度为2的回文
for (int i = 0; i < n - 1; i++) {
isPal[i][i + 1] = (s.charAt(i) == s.charAt(i + 1));
}
// 处理长度≥3的回文
for (int len = 3; len <= n; len++) {
for (int i = 0; i + len <= n; i++) {
int j = i + len - 1;
isPal[i][j] = (s.charAt(i) == s.charAt(j) && isPal[i + 1][j - 1]);
}
}
// 步骤2:动态规划
int[] dp = new int[n + 1];
// 初始条件:最坏情况,每个字符单独分割
for (int i = 0; i <= n; i++) {
dp[i] = i - 1;
}
// 状态转移:遍历前j个字符,尝试所有分割点i
for (int j = 1; j <= n; j++) {
for (int i = 0; i < j; i++) {
if (isPal[i][j - 1]) { // s[i..j-1]是回文
dp[j] = Math.min(dp[j], dp[i] + 1);
}
}
}
return dp[n];
}
}
关键逻辑解析
- 回文预处理 :通过动态规划预处理所有子串的回文性,避免每次判断回文时重复计算,时间复杂度
O(n²)
。 - 动态规划状态 :
dp[j]
表示前j
个字符的最少分割次数,利用已计算的dp[i]
快速推导,时间复杂度O(n²)
。 - 初始条件优化 :
dp[0] = -1
是为了让dp[1]
的计算更自然(dp[0] + 1 = 0
,对应单个字符无需分割)。
该方法通过 预处理+动态规划 高效解决问题,时间复杂度为 O(n²)
,可处理题目中 n ≤ 2000
的规模。核心是将"回文判断"和"分割次数计算"解耦,通过预处理降低重复判断的开销,再利用动态规划的状态转移快速推导最优解。