给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是回文。
返回符合要求的 最少分割次数 。
困难
点击在LeetCode中查看题目
输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
输入:s = "a"
输出:0
输入:s = "ab"
输出:1
1 <= s.length <= 2000
s
仅由小写英文字母组成这道题是"分割回文串"的进阶版,要求找到最少的分割次数,使得分割后的每个子串都是回文串。我们可以使用动态规划来解决这个问题。
关键点:
具体步骤:
时间复杂度:O(n2),其中n是字符串的长度。需要O(n2)的时间预处理回文串,以及O(n^2)的时间计算最少分割次数。
空间复杂度:O(n2),需要O(n2)的空间存储isPalindrome数组,以及O(n)的空间存储dp数组。
我们可以对方法一进行优化,减少空间复杂度。
关键点:
具体步骤:
时间复杂度: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 |
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];
}
}
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]
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 | 执行速度最快,内存消耗最低 |
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
动态规划(预处理回文串) | O(n^2) | O(n^2) | 实现简单,思路清晰 | 空间复杂度较高 |
优化的动态规划(中心扩展法) | O(n^2) | O(n) | 空间复杂度较低 | 实现稍复杂 |
回溯(暴力枚举) | O(2^n) | O(n) | 思路直观 | 时间复杂度高,会超时 |