LeetCode第132题_分割回文串II

LeetCode 第132题:分割回文串 II

题目描述

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。

返回符合要求的 最少分割次数

难度

困难

题目链接

点击在LeetCode中查看题目

示例

示例 1:

复制代码
输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。

示例 2:

复制代码
输入:s = "a"
输出:0

示例 3:

复制代码
输入:s = "ab"
输出:1

提示

  • 1 <= s.length <= 2000
  • s 仅由小写英文字母组成

解题思路

方法一:动态规划

这道题是"分割回文串"的进阶版,要求找到最少的分割次数,使得分割后的每个子串都是回文串。我们可以使用动态规划来解决这个问题。

关键点:

  • 使用动态规划预处理判断子串是否为回文串
  • 使用动态规划计算最少分割次数

具体步骤:

  1. 预处理判断子串是否为回文串
    • 定义isPalindrome[i][j]表示s[i...j]是否为回文串
    • 状态转移方程:isPalindrome[i][j] = (s[i] == s[j]) && (j - i <= 1 || isPalindrome[i+1][j-1])
  2. 计算最少分割次数
    • 定义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)
  3. 返回dp[n-1],其中n是字符串的长度

时间复杂度:O(n2),其中n是字符串的长度。需要O(n2)的时间预处理回文串,以及O(n^2)的时间计算最少分割次数。

空间复杂度:O(n2),需要O(n2)的空间存储isPalindrome数组,以及O(n)的空间存储dp数组。

方法二:优化的动态规划

我们可以对方法一进行优化,减少空间复杂度。

关键点:

  • 使用中心扩展法判断回文串,避免使用O(n^2)的空间
  • 使用动态规划计算最少分割次数

具体步骤:

  1. 定义dp[i]表示s[0...i]的最少分割次数
  2. 初始化dp[i] = i(最坏情况下,每个字符都是一个回文串,需要i次分割)
  3. 对于每个位置i,以i为中心向两边扩展,找到所有以i为中心的回文串
    • 对于奇数长度的回文串,从i向两边扩展
    • 对于偶数长度的回文串,从i和i+1向两边扩展
  4. 对于每个找到的回文串s[j...i],更新dp[i] = min(dp[i], dp[j-1] + 1)
  5. 如果s[0...i]是回文串,则dp[i] = 0
  6. 返回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 执行速度最快,内存消耗最低

代码亮点

  1. 🎯 使用动态规划预处理回文串,避免重复计算
  2. 💡 巧妙利用dp数组存储最少分割次数,状态转移清晰
  3. 🔍 优化判断条件,当s[0...i]是回文串时直接设置dp[i] = 0
  4. 🎨 代码结构清晰,逻辑简单易懂

常见错误分析

  1. 🚫 预处理回文串的状态转移方程错误,导致判断回文串不正确
  2. 🚫 初始化dp数组不正确,影响最终结果
  3. 🚫 没有考虑s[0...i]是回文串的特殊情况,导致计算错误
  4. 🚫 遍历顺序错误,导致状态转移不正确

解法对比

解法 时间复杂度 空间复杂度 优点 缺点
动态规划(预处理回文串) O(n^2) O(n^2) 实现简单,思路清晰 空间复杂度较高
优化的动态规划(中心扩展法) O(n^2) O(n) 空间复杂度较低 实现稍复杂
回溯(暴力枚举) O(2^n) O(n) 思路直观 时间复杂度高,会超时

相关题目

相关推荐
审计侠14 分钟前
Go语言-初学者日记(八):构建、部署与 Docker 化
开发语言·后端·golang
kovlistudio15 分钟前
红宝书第二十九讲:详解编辑器和IDE:VS Code与WebStorm
开发语言·前端·javascript·ide·学习·编辑器·webstorm
东方珵蕴32 分钟前
Logo语言的区块链
开发语言·后端·golang
uhakadotcom44 分钟前
PyTorch 分布式训练入门指南
算法·面试·github
明月醉窗台1 小时前
Qt 入门 1 之第一个程序 Hello World
开发语言·c++·qt
uhakadotcom1 小时前
PyTorch 与 Amazon SageMaker 配合使用:基础知识与实践
算法·面试·github
Craaaayon1 小时前
Java八股文-List集合
java·开发语言·数据结构·list
uhakadotcom1 小时前
在Google Cloud上使用PyTorch:如何在Vertex AI上训练和调优PyTorch模型
算法·面试·github
幻想趾于现实1 小时前
C# Winform 入门(11)之制作酷炫灯光效果
开发语言·c#·winform
hy____1231 小时前
类与对象(中)(详解)
开发语言·c++