欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 创投人物 > 【动态规划】 深入动态规划 回文子串问题

【动态规划】 深入动态规划 回文子串问题

2025/4/19 6:57:57 来源:https://blog.csdn.net/2403_87459748/article/details/147078910  浏览:    关键词:【动态规划】 深入动态规划 回文子串问题

文章目录

  • 前言
  • 例题
    • 一、回文子串
    • 二、 最长回文子串
    • 三、回文串分割IV
    • 四、分割回文串II
    • 五、最长回文子序列
    • 六、让字符串成为回文串的最小插入次数
  • 结语


在这里插入图片描述

前言

那么,什么是动态规划中的回文子串问题呢?

动态规划中的回文子串问题是一个经典的字符串处理问题。具体描述为:给定一个字符串,要求找出该字符串中所有的回文子串。回文子串是指从左到右和从右到左读起来都一样的字符串片段。
例如,对于字符串 “abcba”,其中的 “a”“b”“c”“aba”“bcb”“abcba” 都是回文子串。
解决这个问题通常使用动态规划的方法。定义一个二维布尔数组dp[i][j],表示字符串中从索引i到索引j的子串是否为回文子串。初始化时,单个字符一定是回文子串,即dp[i][i] = true。然后,从长度为 2 的子串开始逐步计算到整个字符串的长度。对于长度大于 1 的子串,如果两端字符相等,即s[i] == s[j],并且去掉两端字符后的子串也是回文子串,即dp[i + 1][j - 1] = true,那么这个子串就是回文子串,dp[i][j] = true。通过这样的动态规划过程,可以高效地找出字符串中的所有回文子串,时间复杂度通常为O(n2),其中n是字符串的长度。

下面本文将通过以例题的形式详细阐述动态规划中的回文子串问题。

例题

一、回文子串

  1. 题目链接:回文子串
  2. 题目描述:

给你⼀个字符串 s ,请你统计并返回这个字符串中 回文字串的数目。 回文字符串 是正着读和倒过来读⼀样的字符串。 ⼦字符串 是字符串中的由连续字符组成的⼀个序列。 具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = “abc”
输出:3
解释:三个回文子串: “a”, “b”, “c”
示例 2:
输入:s = “aaa”
输出:6
解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”
提示:1 <= s.length <= 1000
s 由小写英文字母组成

  1. 解法(动态规划): 算法思路: 我们可以先「预处理」⼀下,将所有子串「是否回文」的信息统计在 dp 表里面,然后直接在表里面统计 true 的个数即可。
    状态表示:为了能表示出来所有的子串,我们可以创建⼀个 n * n 的⼆维 dp 表,只用到「上三角部分」 即可。 其中, dp[i][j] 表示: s 字符串 [i, j] 的子串,是否是回文串。
    状态转移方程: 对于回文串,我们一般分析一个「区间两头」的元素:
    i. 当 s[i] != s[j] 的时候:不可能是回⽂串, dp[i][j] = 0 ;
    ii. 当 s[i] == s[j] 的时候:根据⻓度分三种情况讨论:
    • 长度为 1 ,也就是 i == j :此时⼀定是回文串, dp[i][j] = true ;
    • 长度为 2 ,也就是 i + 1 == j :此时也⼀定是回文串, dp[i][j] = true ;
    • 长度大于 2 ,此时要去看看 [i + 1, j - 1] 区间的子串是否回文: dp[i][j] = dp[i + 1][j - 1] 。 综上,状态转移方程分情况谈论即可。
    初始化:因为我们的状态转移方程分析的很细致,因此无需初始化。
    填表顺序: 根据「状态转移方程」,我们需要「从下往上」填写每⼀行,每⼀行的顺序无所谓。
    返回值: 根据「状态表示和题母要求」,我们需要返回 dp 表中 true 的个数。

  2. 代码示例:

 public int countSubstrings(String s) {// 1. 创建 dp 表// 2. 初始化// 3. 填表// 4. 返回值int n = s.length();boolean[][] dp = new boolean[n][n];int ret = 0;for (int i = n - 1; i >= 0; i--) {for (int j = i; j < n; j++) {if (s.charAt(i) == s.charAt(j))dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true;if (dp[i][j])ret++;}}return ret;}

二、 最长回文子串

  1. 题目链接:最长回文子串
  2. 题目描述:

给你⼀个字符串 s,找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1: 输入:s = “babad” 输出:“bab” 解释:“aba” 同样是符合题意的答案。
示例 2: 输入:s = “cbbd” 输出:“bb”
提示:1 <= s.length <= 1000
s 仅由数字和英文字母组成

  1. 解法(动态规划):
    算法思路:
    a. 我们可以先⽤ dp 表统计出「所有子串是否回文」的信息
    b. 然后根据 dp 表示 true 的位置,得到回文串的「起始位置」和「长度」。
    那么我们就可以在表中找出最长回文串

  2. 代码示例:

public String longestPalindrome(String s) {// 1. 创建 dp 表// 2. 初始化// 3. 填表// 4. 返回值int n = s.length();boolean[][] dp = new boolean[n][n];int begin = 0, len = 1; // 标记⼀下最⻓⼦串的起始位置和⻓度for (int i = n - 1; i >= 0; i--) {for (int j = i; j < n; j++) {if (s.charAt(i) == s.charAt(j))dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true;if (dp[i][j] && j - i + 1 > len) {len = j - i + 1;begin = i;}}}return s.substring(begin, begin + len);}

三、回文串分割IV

  1. 题目链接:回文串分割IV
  2. 题目描述:

给你⼀个字符串 s ,如果可以将它分割成三个非空回文子字符串,那么返回 true ,否则返 回 false 。 当⼀个字符串正着读和反着读是⼀模⼀样的,就称其为回文字符串 。
示例 1: 输入:s = “abcbdd” 输出:true
解释:“abcbdd” = “a” + “bcb” + “dd”,三个⼦字符串都是回⽂的。
示例 2: 输入:s = “bcbddxy” 输出:false
解释:s 没办法被分割成 3 个回文子字符串。
提⽰: 3 <= s.length <= 2000
s 只包含⼩写英⽂字⺟。

  1. 解法(动态规划):
    算法思路: 题目要求⼀个字符串被分成「三个非空回文子串」,乍⼀看,要表⽰的状态很多,有些无从下手。 其实,我们可以把它拆成「两个小问题」:
    i. 动态规划求解字符串中的⼀段非空子串是否是回文串;
    ii. 枚举三个子串除字符串端点外的起止点,查询这三段非空子串是否是回文串。 那么这道困难题就免秒变为简单题啦,变成了⼀道枚举题。

  2. 代码示例:

 public boolean checkPartitioning(String s) {// 1. 利⽤ dp 处理⼀下所有的⼦串是否回⽂int n = s.length();boolean[][] dp = new boolean[n][n];for (int i = n - 1; i >= 0; i--)for (int j = i; j < n; j++)if (s.charAt(i) == s.charAt(j))dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true;// 2. 枚举第⼆个字符串所有的起始位置和终⽌位置for (int i = 1; i < n - 1; i++)for (int j = i; j < n - 1; j++)if (dp[0][i - 1] && dp[i][j] && dp[j + 1][n - 1])return true;return false;}

四、分割回文串II

  1. 题目链接:分割回文串
  2. 题目描述:

给你⼀个字符串 s,请你将 s 分割成⼀些⼦串,使每个子串都是回文。 返回符合要求的最少分割次数 。
示例 1:输入:s = “aab” 输出:1
解释:只需⼀次分割就可将 s 分割成 [“aa”,“b”] 这样两个回⽂⼦串。
示例 2:输入:s = “a” 输出:0
示例 3:输入:s = “ab” 输出:1

  1. 解法(动态规划):
    算法思路:
    状态表⽰: 根据「经验」,继续尝试用 i 位置为结尾,定义状态表示,看看能否解决问题:
    dp[i] 表⽰: s 中 [0, i] 区间上的字符串,最少分割的次数。
    状态转移⽅程: 状态转移⽅程⼀般都是根据「最后⼀个位置」的信息来分析:设 0 <= j <= i ,那么我们可以 根据 j ~ i 位置上的⼦串是否是回⽂串分成下面两类:
    i. 当 [j ,i] 位置上的⼦串能够构成⼀个回文串,那么 dp[i] 就等于 [0, j - 1] 区 间上最少回文串的个数 + 1,即 dp[i] = dp[j - 1] + 1 ;
    ii. 当 [j ,i] 位置上的子串不能构成⼀个回文串,此时 j 位置就不用考虑。 由于我们要的是最⼩值,因此应该循环遍历⼀遍 j 的取值,拿到里面的最小值即可。 优化:我们在状态转移方程里面分析到,要能够快速判读字符串里面的子串是否回文。因此,我们 可以先处理⼀个 dp 表,里面保存所有子串是否回文的信息。
    初始化: 观察「状态转移方程」,我们会用到 j - 1 位置的值。我们可以思考⼀下当 j == 0 的时候, 表⽰的区间就是 [0, i] 。如果 [0, i] 区间上的字符串已经是回文串了,最⼩的回⽂串就是 1 了, j 往后的值就不⽤遍历了。 因此,我们可以在循环遍历 j 的值之前处理 j == 0 的情况,然后 j 从 1 开始循环。 但是,为了防止求 min 操作时, 0 干扰结果。我们先把表里面的值初始化为「无穷大」。
    填表顺序: 毫无疑问是「从左往右」。
    返回值: 根据「状态表示」,应该返回 dp[n - 1]

  2. 代码示例:

 public int minCut(String s) {// 1. 预处理int n = s.length();boolean[][] isPal = new boolean[n][n];for (int i = n - 1; i >= 0; i--)for (int j = i; j < n; j++)if (s.charAt(i) == s.charAt(j))isPal[i][j] = i + 1 < j ? isPal[i + 1][j - 1] : true;int[] dp = new int[n];for (int i = 0; i < n; i++) dp[i] = Integer.MAX_VALUE;for (int i = 0; i < n; i++) {if (isPal[0][i]) dp[i] = 0;else {for (int j = 1; j <= i; j++)if (isPal[j][i])dp[i] = Math.min(dp[i], dp[j - 1] + 1);}}return dp[n - 1];}

五、最长回文子序列

  1. 题目链接:最长回文子序列
  2. 题目描述:

给你⼀个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。 子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的⼀个序列。
示例 1:输入:s = “bbbab” 输出:4 解释:⼀个可能的最长回文子序列为 “bbbb” 。
示例 2:输入:s = “cbbd” 输出:2 解释:⼀个可能的最长回文子序列为 “bb” 。

  1. 解法(动态规划):
    算法思路:
    状态表示: 关于「单个字符串」问题中的「回文子序列」,或者「回文子串」,我们的状态表⽰研究的对象⼀ 般都是选取原字符串中的⼀段区域 [i, j] 内部的情况。这里我们继续选取字符串中的⼀段区域 来研究:
    dp[i][j] 表示:s 字符串 [i, j] 区间内的所有的⼦序列中,最长的回文子序列的长度。
    状态转移⽅程: 关于「回文子序列」和「回文子串」的分析方式,⼀般都是比较固定的,都是选择这段区域的「左 右端点」的字符情况来分析。因为如果⼀个序列示回文串的话,「去掉首尾两个元素之后依旧是回文串」,「⾸尾加上两个相同的元素之后也依旧是回文串」。因为,根据「首尾元素」的不同,可以分为下面两种情况:
    i. 当⾸尾两个元素「相同」的时候,也就是 s[i] == s[j] :那么 [i, j] 区间上的最长回文子序列,应该是 [i + 1, j - 1] 区间内的那个最长回文子序列首尾填上=s[i] 和 s[j] ,此时 dp[i][j] = dp[i + 1][j - 1] + 2
    ii. 当首尾两个元素不「相同」的时候,也就是 s[i] != s[j] :此时这两个元素就不能同 时添加在⼀个回文串的左右,那么我们就应该让 s[i] 单独加在⼀个序列的左边,或者 让 s[j] 单独放在⼀个序列的右边,看看这两种情况下的最大值:
    • 单独加入 s[i] 后的区间在 [i, j - 1] ,此时最长的回文序列的长度就是 dp[i]
    [j - 1] ;
    • 单独加⼊ s[j] 后的区间在 [i + 1, j] ,此时最长的回文序列的长度就是 dp[i + 1][j] ; 取两者的最⼤值,于是 dp[i][j] =max(dp[i][j - 1], dp[i + 1][j])
    综上所述,状态转移⽅程为:
    ▪ 当 s[i] == s[j] 时: dp[i][j] = dp[i + 1][j - 1] + 2
    ▪ 当 s[i] != s[j] 时: dp[i][j] = max(dp[i][j - 1], dp[i + 1][j])
    初始化: 我们的初始化⼀般就是为了处理在状态转移的过程中,遇到的⼀些边界情况,因为我们需要根据状 态转移⽅程来分析哪些位置需要初始化。 根据状态转移⽅程 dp[i][j] = dp[i + 1][j - 1] + 2 ,我们状态表⽰的时候,选取的是⼀段区间,因此需要要求左端点的值要小于等于右端点的值,因此会有两种边界情况:
    i. 当 i == j 的时候, i + 1 就会大于 j - 1 ,此时区间内只有⼀个字符。这个比较好 分析, dp[i][j] 表示⼀个字符的最⻓回⽂序列,⼀个字符能够自己组成回文串,因此此
    时 dp[i][j] = 1 ;
    ii. 当 i + 1 == j 的时候, i + 1 也会大于 j - 1 ,此时区间内有两个字符。这样也好分析,当这两个字符相同的时候, dp[i][j] = 2 ;不相同的时候, d[i][j] = 0 。 对于第⼀种边界情况,我们在填表的时候,就可以同步处理。 对于第⼆种边界情况, dp[i + 1][j - 1] 的值为 0 ,不会影响最终的结果,因此可以不用考虑。
    填表顺序: 根据「状态转移」,我们发现,在 dp 表所表示的矩阵中, dp[i + 1] 表示下⼀行的位置,
    dp[j - 1] 表示前⼀列的位置。因此我们的填表顺序应该是「从下往上填写每⼀行」,「每⼀行从左往右」。
    这个与我们⼀般的填写顺序不太⼀致。
    返回值: 根据「状态表示」,我们需要返回 [0, n -1] 区域上的最长回文序列的长度,因此需要返回dp[0][n - 1] 。

  2. 代码示例:

public int longestPalindromeSubseq(String s) {// 1. 创建 dp 表// 2. 初始化// 3. 填表// 4. 返回结果int n = s.length();int[][] dp = new int[n][n];for (int i = n - 1; i >= 0; i--) {dp[i][i] = 1;for (int j = i + 1; j < n; j++)if (s.charAt(i) == s.charAt(j)) dp[i][j] = dp[i + 1][j - 1] + 2;else dp[i][j] = Math.max(dp[i][j - 1], dp[i + 1][j]);}return dp[0][n - 1];}

六、让字符串成为回文串的最小插入次数

  1. 题目链接: 让字符串成为回文串的最小插入次数
  2. 题目描述:

给你⼀个字符串 s ,每⼀次操作你都可以在字符串的任意位置插⼊任意字符。 请你返回让 s 成为回⽂串的 最少操作次数 。 「回⽂串」是正读和反读都相同的字符串。
示例 1: 输⼊:s = “zzazz” 输出:0
解释:字符串 “zzazz” 已经是回⽂串了,所以不需要做任何插⼊操作。
示例 2: 输入:s = “mbadm” 输出:2 解释:字符串可变为 “mbdadbm” 或者 “mdbabdm” 。
示例 3: 输入:s = “leetcode”
输出:5 解释:插入 5 个字符后字符串变为 “leetcodocteel” 。 提⽰: 1 <= s.length <= 500
s 中所有字符都是小写字母。

  1. 解法(动态规划): 算法思路:
    状态表⽰: 关于「单个字符串」问题中的「回文子序列」,或者「回文子串」,我们的状态表⽰研究的对象⼀ 般都是选取原字符串中的⼀段区域 [i, j] 内部的情况。这里我们继续选取字符串中的⼀段区域来研究: 状态表示: dp[i][j] 表⽰字符串 [i, j] 区域成为回文子串的最少插入次数。
    状态转移方程: 关于「回文子序列」和「回文子串」的分析方式,⼀般都是比较固定的,都是选择这段区域的「左右端点」的字符情况来分析。因为如果⼀个序列是回文串的话,「去掉首尾两个元素之后依旧是回文串」,「首尾加上两个相同的元素之后也依旧是回文串」。因为,根据「首尾元素」的不同,可以分为下面两种情况:
    i. 当⾸尾两个元素「相同」的时候,也就是 s[i] == s[j] :
    那么 [i, j] 区间内成为回⽂⼦串的最少插入次数,取决于 [i + 1, j - 1] 区间 内成为回⽂⼦串的最少插入次数;
    若 i == j 或 i == j - 1 ( [i + 1, j - 1] 不构成合法区间),此时只有 1 ~ 2 个相同的字符, [i, j] 区间⼀定是回⽂⼦串,成为回⽂⼦串的最少插⼊次数是 0。 此时 dp[i][j] = i >= j - 1 ? 0 : dp[i + 1][j - 1] ;
    ii. 当⾸尾两个元素「不相同」的时候,也就是 s[i] != s[j] :
    此时可以在区间最右边补上⼀个 s[i] ,需要的最少插⼊次数是 [i + 1, j] 成为回文子串的最少插入次数 + 本次插⼊,即 dp[i][j] = dp[i + 1][j] + 1 ;
    此时可以在区间最左边补上⼀个 s[j] ,需要的最少插⼊次数是 [i, j + 1] 成为回文子串的最少插入次数 + 本次插⼊,即 dp[i][j] = dp[i][j + 1] + 1 ; 综上所述,状态转移⽅程为:
    ▪ 当 s[i] == s[j] 时: dp[i][j] = i >= j - 1 ? 1 : dp[i + 1][j - 1];
    ▪ 当 s[i] != s[j] 时: dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + 1 。
    初始化: 根据「状态转移方程」,没有不能递推表示的值。无需初始化。
    填表顺序: 根据「状态转移」,我们发现,在 dp 表所表示的矩阵中, dp[i + 1] 表示下一行的位置,
    dp[j - 1] 表示前⼀列的位置。因此我们的填表顺序应该是「从下往上填写每一行」,「每一行从左往右」。
    这个与我们⼀般的填写顺序不太⼀致。
    返回值: 根据「状态表示」,我们需要返回 [0, n -1] 区域上成为回文子串的最少插入次数,因此需要返回 dp[0][n - 1] 。

  2. 代码示例:

public int minInsertions(String s) {int n = s.length();int[][] dp = new int[n][n]; // 创建 dp 表for (int i = n - 1; i >= 0; i--)for (int j = i + 1; j < n; j++)if (s.charAt(i) == s.charAt(j)) dp[i][j] = dp[i + 1][j - 1];else dp[i][j] = Math.min(dp[i][j - 1], dp[i + 1][j]) + 1;return dp[0][n - 1];}

结语

本文到这里就结束了,主要通过几道回文子串算法题,介绍了这种类型动态规划的做题思路,带大家深入了解了动态规划中回文子串算法这一类型。

以上就是本文全部内容,感谢各位能够看到最后,如有问题,欢迎各位大佬在评论区指正,希望大家可以有所收获!创作不易,希望大家多多支持!

最后,大家再见!祝好!我们下期见!

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词