Hard
题目描述
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。
返回符合要求的最少分割次数。
示例 1:
输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
示例 2:
输入:s = "a"
输出:0
示例 3:
输入:s = "ab"
输出:1
提示:
1 <= s.length <= 2000s仅由小写英文字母组成
解题思路
解题思路
这是一道经典的动态规划题目,需要找到将字符串分割成回文子串的最少分割次数。
方法一:动态规划 + 预处理(推荐)
核心思想是使用两个动态规划:
- 首先预处理出所有子串是否为回文串
- 然后用动态规划求最少分割次数
预处理阶段:
- 使用二维数组
isPalindrome[i][j]表示子串s[i:j+1]是否为回文串 - 通过中心扩展法或区间DP预处理所有子串的回文性质
动态规划阶段:
- 定义
dp[i]表示字符串s[0:i]的最少分割次数 - 状态转移:对于每个位置
i,枚举所有可能的分割点j - 如果
s[j:i]是回文串,则dp[i] = min(dp[i], dp[j] + 1)
方法二:中心扩展优化
在计算最少分割次数的同时,使用中心扩展法动态判断回文串,可以节省空间。
时间复杂度:O(n²),空间复杂度:O(n²)(预处理)或O(n)(中心扩展)
代码实现
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++) {
isPalindrome[i][i] = true;
}
// 长度为2的子串
for (int i = 0; i < n - 1; i++) {
if (s[i] == s[i + 1]) {
isPalindrome[i][i + 1] = true;
}
}
// 长度大于2的子串
for (int len = 3; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
if (s[i] == s[j] && isPalindrome[i + 1][j - 1]) {
isPalindrome[i][j] = true;
}
}
}
// 动态规划求最少分割次数
vector<int> dp(n, INT_MAX);
for (int i = 0; i < n; i++) {
if (isPalindrome[0][i]) {
dp[i] = 0; // 整个前缀都是回文串,不需要分割
} else {
for (int j = 0; j < i; j++) {
if (isPalindrome[j + 1][i]) {
dp[i] = 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):
is_palindrome[i][i] = True
# 长度为2的子串
for i in range(n - 1):
if s[i] == s[i + 1]:
is_palindrome[i][i + 1] = True
# 长度大于2的子串
for length in range(3, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j] and is_palindrome[i + 1][j - 1]:
is_palindrome[i][j] = True
# 动态规划求最少分割次数
dp = [float('inf')] * n
for i in range(n):
if is_palindrome[0][i]:
dp[i] = 0 # 整个前缀都是回文串,不需要分割
else:
for j in range(i):
if is_palindrome[j + 1][i]:
dp[i] = min(dp[i], dp[j] + 1)
return dp[n - 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++) {
isPalindrome[i, i] = true;
}
// 长度为2的子串
for (int i = 0; i < n - 1; i++) {
if (s[i] == s[i + 1]) {
isPalindrome[i, i + 1] = true;
}
}
// 长度大于2的子串
for (int len = 3; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
if (s[i] == s[j] && isPalindrome[i + 1, j - 1]) {
isPalindrome[i, j] = true;
}
}
}
// 动态规划求最少分割次数
int[] dp = new int[n];
Array.Fill(dp, int.MaxValue);
for (int i = 0; i < n; i++) {
if (isPalindrome[0, i]) {
dp[i] = 0; // 整个前缀都是回文串,不需要分割
} else {
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];
}
}
var minCut = function(s) {
const n = s.length;
// 预处理:判断所有子串是否为回文串
const isPalindrome = Array(n).fill().map(() => Array(n).fill(false));
// 单个字符都是回文串
for (let i = 0; i < n; i++) {
isPalindrome[i][i] = true;
}
// 长度为2的子串
for (let i = 0; i < n - 1; i++) {
if (s[i]
复杂度分析
| 项目 | 复杂度 |
|---|---|
| 时间复杂度 | O(n²) |
| 空间复杂度 | O(n²) |
说明:
- 时间复杂度:预处理回文串判断需要 O(n²),动态规划过程也需要 O(n²)
- 空间复杂度:需要 O(n²) 的空间存储回文串判断结果,以及 O(n) 的空间存储dp数组