LeetCode 第132题:分割回文串 II
题目描述
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是回文。
返回符合要求的 最少分割次数 。
难度
困难
题目链接
示例
示例 1:
输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
示例 2:
输入:s = "a"
输出:0
示例 3:
输入:s = "ab"
输出:1
提示
1 <= s.length <= 2000
s
仅由小写英文字母组成
解题思路
方法一:动态规划
这道题是"分割回文串"的进阶版,要求找到最少的分割次数,使得分割后的每个子串都是回文串。我们可以使用动态规划来解决这个问题。
关键点:
- 使用动态规划预处理判断子串是否为回文串
- 使用动态规划计算最少分割次数
具体步骤:
- 预处理判断子串是否为回文串
- 定义isPalindrome[i][j]表示s[i...j]是否为回文串
- 状态转移方程:isPalindrome[i][j] = (s[i] == s[j]) && (j - i <= 1 || isPalindrome[i+1][j-1])
- 计算最少分割次数
- 定义dp[i]表示s[0...i]的最少分割次数
- 初始化dp[i] = i(最坏情况下,每个字符都是一个回文串,需要i次分割)
- 状态转移方程:
- 如果s[0...i]是回文串,则dp[i] = 0(不需要分割)
- 否则,遍历j从0到i-1,如果s[j+1...i]是回文串,则dp[i] = min(dp[i], dp[j] + 1)
- 返回dp[n-1],其中n是字符串的长度
时间复杂度:O(n2),其中n是字符串的长度。需要O(n2)的时间预处理回文串,以及O(n^2)的时间计算最少分割次数。
空间复杂度:O(n2),需要O(n2)的空间存储isPalindrome数组,以及O(n)的空间存储dp数组。
方法二:优化的动态规划
我们可以对方法一进行优化,减少空间复杂度。
关键点:
- 使用中心扩展法判断回文串,避免使用O(n^2)的空间
- 使用动态规划计算最少分割次数
具体步骤:
- 定义dp[i]表示s[0...i]的最少分割次数
- 初始化dp[i] = i(最坏情况下,每个字符都是一个回文串,需要i次分割)
- 对于每个位置i,以i为中心向两边扩展,找到所有以i为中心的回文串
- 对于奇数长度的回文串,从i向两边扩展
- 对于偶数长度的回文串,从i和i+1向两边扩展
- 对于每个找到的回文串s[j...i],更新dp[i] = min(dp[i], dp[j-1] + 1)
- 如果s[0...i]是回文串,则dp[i] = 0
- 返回dp[n-1]
时间复杂度:O(n^2),其中n是字符串的长度。
空间复杂度:O(n),只需要O(n)的空间存储dp数组。
图解思路
动态规划过程分析表
以示例1为例:s = "aab"
预处理回文串
isPalindrome[i][j] | j=0 | j=1 | j=2 |
---|---|---|---|
i=0 | true | true | false |
i=1 | - | true | false |
i=2 | - | - | true |
计算最少分割次数
i | s[0...i] | dp[i]初始值 | 计算过程 | 最终dp[i] | 说明 |
---|---|---|---|---|---|
0 | "a" | 0 | s[0...0]是回文串,dp[0] = 0 | 0 | 单个字符是回文串,不需要分割 |
1 | "aa" | 1 | s[0...1]是回文串,dp[1] = 0 | 0 | "aa"是回文串,不需要分割 |
2 | "aab" | 2 | s[0...2]不是回文串 s[1...2]不是回文串,dp[2] = min(dp[2], dp[0] + 1) = min(2, 0 + 1) = 1 s[2...2]是回文串,dp[2] = min(dp[2], dp[1] + 1) = min(1, 0 + 1) = 1 | 1 | "aab"需要分割一次 |
中心扩展法分析表
中心位置 | 扩展类型 | 找到的回文串 | 更新dp[i] | 说明 |
---|---|---|---|---|
0 | 奇数长度 | "a" | dp[0] = 0 | 单个字符是回文串 |
0 | 偶数长度 | "aa" | dp[1] = 0 | "aa"是回文串 |
1 | 奇数长度 | "a" | dp[1] = min(dp[1], dp[0] + 1) = 0 | dp[1]已经是0,不更新 |
1 | 偶数长度 | "ab" | - | "ab"不是回文串,不更新 |
2 | 奇数长度 | "b" | dp[2] = min(dp[2], dp[1] + 1) = min(2, 0 + 1) = 1 | 更新dp[2] = 1 |
代码实现
C# 实现
csharp
public class Solution {
public int MinCut(string s) {
int n = s.Length;
// 预处理回文串
bool[,] isPalindrome = new bool[n, n];
for (int i = 0; i < n; i++) {
for (int j = 0; j <= i; j++) {
if (s[j] == s[i] && (i - j <= 1 || isPalindrome[j + 1, i - 1])) {
isPalindrome[j, i] = true;
}
}
}
// 计算最少分割次数
int[] dp = new int[n];
for (int i = 0; i < n; i++) {
dp[i] = i; // 最坏情况下,需要i次分割
if (isPalindrome[0, i]) {
dp[i] = 0; // 如果s[0...i]是回文串,不需要分割
continue;
}
for (int j = 0; j < i; j++) {
if (isPalindrome[j + 1, i]) {
dp[i] = Math.Min(dp[i], dp[j] + 1);
}
}
}
return dp[n - 1];
}
}
Python 实现
python
class Solution:
def minCut(self, s: str) -> int:
n = len(s)
# 预处理回文串
is_palindrome = [[False] * n for _ in range(n)]
for i in range(n):
for j in range(i + 1):
if s[j] == s[i] and (i - j <= 1 or is_palindrome[j + 1][i - 1]):
is_palindrome[j][i] = True
# 计算最少分割次数
dp = list(range(n)) # 初始化dp[i] = i
for i in range(n):
if is_palindrome[0][i]:
dp[i] = 0 # 如果s[0...i]是回文串,不需要分割
continue
for j in range(i):
if is_palindrome[j + 1][i]:
dp[i] = min(dp[i], dp[j] + 1)
return dp[n - 1]
C++ 实现
cpp
class Solution {
public:
int minCut(string s) {
int n = s.length();
// 预处理回文串
vector<vector<bool>> isPalindrome(n, vector<bool>(n, false));
for (int i = 0; i < n; i++) {
for (int j = 0; j <= i; j++) {
if (s[j] == s[i] && (i - j <= 1 || isPalindrome[j + 1][i - 1])) {
isPalindrome[j][i] = true;
}
}
}
// 计算最少分割次数
vector<int> dp(n);
for (int i = 0; i < n; i++) {
dp[i] = i; // 最坏情况下,需要i次分割
if (isPalindrome[0][i]) {
dp[i] = 0; // 如果s[0...i]是回文串,不需要分割
continue;
}
for (int j = 0; j < i; j++) {
if (isPalindrome[j + 1][i]) {
dp[i] = min(dp[i], dp[j] + 1);
}
}
}
return dp[n - 1];
}
};
执行结果
C# 实现
- 执行用时:92 ms
- 内存消耗:39.8 MB
Python 实现
- 执行用时:1024 ms
- 内存消耗:31.2 MB
C++ 实现
- 执行用时:56 ms
- 内存消耗:8.7 MB
性能对比
语言 | 执行用时 | 内存消耗 | 特点 |
---|---|---|---|
C# | 92 ms | 39.8 MB | 执行速度适中,内存消耗较高 |
Python | 1024 ms | 31.2 MB | 执行速度较慢,内存消耗适中 |
C++ | 56 ms | 8.7 MB | 执行速度最快,内存消耗最低 |
代码亮点
- 🎯 使用动态规划预处理回文串,避免重复计算
- 💡 巧妙利用dp数组存储最少分割次数,状态转移清晰
- 🔍 优化判断条件,当s[0...i]是回文串时直接设置dp[i] = 0
- 🎨 代码结构清晰,逻辑简单易懂
常见错误分析
- 🚫 预处理回文串的状态转移方程错误,导致判断回文串不正确
- 🚫 初始化dp数组不正确,影响最终结果
- 🚫 没有考虑s[0...i]是回文串的特殊情况,导致计算错误
- 🚫 遍历顺序错误,导致状态转移不正确
解法对比
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
动态规划(预处理回文串) | O(n^2) | O(n^2) | 实现简单,思路清晰 | 空间复杂度较高 |
优化的动态规划(中心扩展法) | O(n^2) | O(n) | 空间复杂度较低 | 实现稍复杂 |
回溯(暴力枚举) | O(2^n) | O(n) | 思路直观 | 时间复杂度高,会超时 |
相关题目
- LeetCode 131. 分割回文串 - 中等
- LeetCode 5. 最长回文子串 - 中等
- LeetCode 647. 回文子串 - 中等
- LeetCode 1745. 回文串分割 IV - 困难
- LeetCode 1278. 分割回文串 III - 困难