LeetCode算法日记 - Day 98: 分割回文串 II

目录

[1. 分割回文串 II](#1. 分割回文串 II)

[1.1 题目解析](#1.1 题目解析)

[1.2 解法](#1.2 解法)

[1.3 代码实现](#1.3 代码实现)


1. 分割回文串 II

https://leetcode.cn/problems/palindrome-partitioning-ii/description/

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

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

示例 1:

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

示例 2:

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

示例 3:

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

提示:

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

1.1 题目解析

题目本质

这是一个字符串分割优化问题,要求将字符串分割成若干个回文子串,并求最少分割次数。本质是在满足所有子串都是回文的约束下,找到分割次数最少的方案。核心挑战是"如何高效判断回文并找到最优分割方案"。

常规解法

最直观的想法:枚举所有可能的分割方案,对每个方案验证所有子串是否为回文,记录最少分割次数。

java 复制代码
// 常规解法:暴力枚举(超时)

static class BruteForceSolution {

    int minCuts = Integer.MAX_VALUE;

    

    public int minCut(String s) {

        dfs(s, 0, 0);

        return minCuts == Integer.MAX_VALUE ? 0 : minCuts;

    }

    

    private void dfs(String s, int start, int cuts) {

        if (start == s.length()) {

            minCuts = Math.min(minCuts, cuts - 1);  // cuts-1 因为最后一段不需要分割

            return;

        }

        

        // 枚举所有可能的回文子串

        for (int end = start; end < s.length(); end++) {

            if (isPalindrome(s, start, end)) {

                dfs(s, end + 1, cuts + 1);

            }

        }

    }

    

    // 判断 s[left..right] 是否为回文

    private boolean isPalindrome(String s, int left, int right) {

        while (left < right) {

            if (s.charAt(left) != s.charAt(right)) {

                return false;

            }

            left++;

            right--;

        }

        return true;

    }

}

问题分析

暴力枚举的复杂度是指数级 O(2^n × n),当 n = 2000 时,2^2000 会严重超时。

关键观察:存在大量重复计算

  • 同一个子串的回文性质会被多次判断
  • 同一个前缀的最少分割次数会被多次计算
  • 可以用动态规划优化

思路转折

要想高效 → 必须避免重复计算 → 动态规划 + 预处理回文信息

**1 .预处理阶段:**用动态规划计算出所有子串 s[i..j] 是否为回文,存入 vis[i][j]

**2. dp 阶段:**用动态规划计算 s[0..i] 的最少分割次数

状态定义:vis[i][j] = s[i..j] 是否为回文,dp[i] = s[0..i] 的最少分割次数

1.2 解法

算法思想

采用动态规划 + 预处理回文信息:

  • 预处理:用区间 dp 计算所有子串的回文性质,vis[i][j] = s[i..j] 是否为回

  • dp 计算:计算每个前缀的最少分割次数

    • 如果 s[0..i] 是回文,则 dp[i] = 0

    • 否则枚举分割点 j,如果 s[j..i] 是回文,则 dp[i] = min(dp[i], dp[j-1] + 1),也就是 i...j 是回文,再检查一下 0....j-1 是否是回文

步骤拆解

**i)边界处理:**长度为 1 直接返回 0

ii)预处理 vis 数组:

  • 创建 boolean[][] vis
  • i 从 n-1 倒序到 0,j 从 i 正序到 n-1
  • 判断 s[i] 是否等于 s[j]:

    • 相等 + 单字符 → vis[i][j] = true

    • 相等 + 相邻 → vis[i][j] = true

    • 相等 + 其他 → vis[i][j] = vis[i+1][j-1]

    • 不相等 → 保持 false

iii)dp 计算:

  • 初始化 dp 数组为 Integer.MAX_VALUE
  • 遍历 i 从 0 到 n-1:

    • 如果 vis[0][i] 为真,则 dp[i] = 0(s[0..i] 是回文)

    • 否则枚举分割点 j(1 ≤ j ≤ i):

      • 如果 vis[j][i] 为真,则 dp[i] = min(dp[i], dp[j-1] + 1),也就是 i...j 是回文,再检查一下 0....j-1 是否是回文

**iv)返回结果:**dp[n-1]

易错点

  • **dp 初始化错误:**应该初始化为 Integer.MAX_VALUE,表示"还未计算"。如果初始化为 0,会被误认为"不需要分割",导致后续计算错误
  • **vis 索引使用错误:**判断 s[j..i] 是否为回文应该用 vis[j][i],不是 vis[i][j]。因为 vis 是上三角矩阵(j ≥ i),如果写反会访问下三角或越界
  • **分割点枚举范围错误:**j 的范围应该是 [1, i],不是 [0, i-1]。因为要判断 s[j..i] 是否为回文,j 必须 ≤ i,且 j=0 的情况已经在 vis[0][i] 中处理了
  • **边界条件遗漏:**如果 s[0..i] 是回文,必须直接设置 dp[i] = 0,不能进入枚举分割点的逻辑。否则可能会错误地计算分割次数

1.3 代码实现

java 复制代码
// 分割回文串 II
static class Solution {
    public int minCut(String s) {
        char[] ch = s.toCharArray();
        int n = ch.length;
        if (n == 1) return 0;  // 边界处理
        
        boolean[][] vis = new boolean[n][n];
        int[] dp = new int[n];
        Arrays.fill(dp, Integer.MAX_VALUE);  // 初始化为未计算状态

        // 预处理:判断所有子串是否为回文
        for (int i = n-1; i >= 0; i--) {
            for (int j = i; j < n; j++) {
                if (ch[i] == ch[j]) {
                    if (i == j) {
                        vis[i][j] = true;  // 单字符
                    } else if (i+1 == j) {
                        vis[i][j] = true;  // 相邻字符
                    } else {
                        vis[i][j] = vis[i+1][j-1];  // 看内部
                    }
                }
            }
        }

        // 动态规划:计算最少分割次数
        for (int i = 0; i < n; i++) {
            if (vis[0][i]) {
                // s[0..i] 是回文,不需要分割
                dp[i] = 0;
            } else {
                // s[0..i] 不是回文,枚举分割点
                for (int j = 1; j <= i; j++) {
                    if (vis[j][i]) {
                        // s[j..i] 是回文,在 j-1 和 j 之间分割
                        dp[i] = Math.min(dp[i], dp[j-1] + 1);
                    }
                }
            }
        }

        return dp[n-1];
    }
}

复杂度分析

  • 时间复杂度:O(n²),
  • 空间复杂度:O(n²)
相关推荐
立志成为大牛的小牛2 小时前
数据结构——三十九、顺序查找(王道408)
数据结构·学习·程序人生·考研·算法
2301_807997382 小时前
代码随想录-day30
数据结构·c++·算法·leetcode
爱代码的小黄人2 小时前
一般角度的旋转矩阵的推导
线性代数·算法·矩阵
ゞ 正在缓冲99%…2 小时前
leetcode1771.由子序列构造的最长回文串长度
数据结构·算法·leetcode
多喝开水少熬夜3 小时前
堆相关算法题基础-java实现
java·开发语言·算法
锂享生活3 小时前
论文阅读:铁路车辆跨临界 CO₂ 空调系统模型预测控制(MPC)策略
论文阅读·算法
B站_计算机毕业设计之家3 小时前
深度学习:Yolo水果检测识别系统 深度学习算法 pyqt界面 训练集测试集 深度学习 数据库 大数据 (建议收藏)✅
数据库·人工智能·python·深度学习·算法·yolo·pyqt
骑自行车的码农3 小时前
React SSR 技术实现原理
算法·react.js
盘古开天16663 小时前
深度强化学习算法详解:从理论到实践
算法