在算法的世界里,回文串问题一直是一个经典且富有挑战性的题目。而动态规划作为一种强大的算法思想,为解决这类问题提供了高效且优雅的解决方案。本文将深入探讨如何运用动态规划算法来解决回文串相关问题,从问题描述、动态规划思路,到代码实现与复杂度分析,全面剖析这一过程。
回文串是指一个字符串从左到右读和从右到左读是完全一样的,例如 “level”、“madam” 等。常见的回文串问题有:给定一个字符串,找出其中最长的回文子串。例如,对于字符串 “babad”,最长回文子串是 “bab” 或 “aba”;对于字符串 “cbbd”,最长回文子串是 “bb”。
dp[i][j]
来表示字符串 s
从索引 i
到 j
的子串是否为回文串。如果 dp[i][j]
为 true
,则表示 s[i...j]
是回文串;否则为 false
。i == j
时,即单个字符,显然是回文串,所以 dp[i][i] = true
。j - i == 1
时,即两个字符的子串,如果这两个字符相等,那么该子串是回文串,即 dp[i][j] = (s[i] == s[j])
。s[i] == s[j]
且 dp[i + 1][j - 1]
为 true
时,说明去掉两端字符后的子串是回文串,那么 dp[i][j] = true
。用公式表示为: if (s[i] == s[j]) {
if (j - i <= 2) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
} else {
dp[i][j] = false;
}
3.遍历顺序:
由于计算 dp[i][j]
需要依赖 dp[i + 1][j - 1]
,所以我们需要从较短的子串开始计算,逐步计算到较长的子串。具体来说,我们先遍历子串的长度 len
,从 1 到字符串的长度 n
,然后遍历子串的起始位置 i
,根据长度 len
计算出结束位置 j = i + len - 1
。
回文子串https://leetcode.cn/problems/palindromic-substrings/description/https://leetcode.cn/problems/palindromic-substrings/description/
给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
示例 1:
输入:s = "abc" 输出:3 解释:三个回文子串: "a", "b", "c"
示例 2:
输入:s = "aaa" 输出:6 解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
1.状态表示
dp[i][j]:表示字符串在区间(i,j)内的子串,是否是回文串。(以i位置表示开始,j位置表示结束)
注意!!! 这里有一个隐藏条件: i <= j 即只用到矩阵上三角部分
2.状态转移方程
对于dp[i][j]位置的状态转移方程分类讨论如下:
(1)s[i] != s[j] 则dp[i][j] = false;
(2) s[i] == s[j],根据子串长度分类讨论如下
3.初始化
在状态转移方程分类讨论时,已经对方程的边界情况做了说明,故此题初始化可以省略。
4.填表顺序
由状态转移方程可以知道,在填dp[i][j]时,用到dp[i+1][j-1],故填表顺序应该从下往上填写每一行,从左往右填写每一行。
5.返回值
返回dp表中true的个数
int countSubstrings(string s)
{
int n = s.size();
vector> dp(n,vector (n));
int ret = 0;
for(int i = n-1; i >= 0; i--)
{
for(int j = i; j < n; j++)
{
if(s[i] == s[j])
{
dp[i][j] =i + 1 < j ? dp[i+1][j-1] : true;
}
if(dp[i][j])
ret++;
}
}
return ret;
}
最长回文子串https://leetcode.cn/problems/longest-palindromic-substring/description/https://leetcode.cn/problems/longest-palindromic-substring/description/
给你一个字符串 s
,找到 s
中最长的 回文子串。
示例 1:输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。
示例 2:输入:s = "cbbd" 输出:"bb"
判断子串是否是回文---用dp表统计所有子串是否是回文的信息----根据dp表的起始位置得到结果
dp[i][j]:表示字符串在区间(i,j)内的子串,是否是回文串。(以i位置表示开始,j位置表示结束)
注意!!! 这里有一个隐藏条件: i <= j 即只用到矩阵上三角部分
2.状态转移方程
对于dp[i][j]位置的状态转移方程分类讨论如下:
(1)s[i] != s[j] 则dp[i][j] = false;
(2) s[i] == s[j],根据子串长度分类讨论如下
3.初始化
在状态转移方程分类讨论时,已经对方程的边界情况做了说明,故此题初始化可以省略。
4.填表顺序
由状态转移方程可以知道,在填dp[i][j]时,用到dp[i+1][j-1],故填表顺序应该从下往上填写每一行,从左往右填写每一行。
5.返回值
dp表中值为true的情况下,长度最大的回文字串的起始位置以及长度
string longestPalindrome(string s) {
int n = s.size();
vector> dp(n,vector(n));
int len = 1,begin = 0;
for(int i = n-1; i >= 0; i--)
{
for(int j = i; j < n; j++)
{
if(s[i] == s[j])
{
dp[i][j] = i+1 < j ? dp[i+1][j-1] : true;
}
if(dp[i][j] == true && j-i +1 > len)
{
len = j-i +1,begin = i;
}
}
}
return s.substr(begin,len);
}
回文串分割Ⅳhttps://leetcode.cn/problems/palindrome-partitioning-iv/description/https://leetcode.cn/problems/palindrome-partitioning-iv/description/
给你一个字符串 s
,如果可以将它分割成三个 非空 回文子字符串,那么返回 true
,否则返回 false
。当一个字符串正着读和反着读是一模一样的,就称其为 回文字符串 。
示例 1:
输入:s = "abcbdd" 输出:true 解释:"abcbdd" = "a" + "bcb" + "dd",三个子字符串都是回文的。
示例 2:
输入:s = "bcbddxy" 输出:false 解释:s 没办法被分割成 3 个回文子字符串。
首先用dp表保存所有字串是否是回文串的信息,对字符串通过i,j 位置可以分割为(0,i-1)、(i,j)、(j+1,n-1)三部分,枚举所有中间字符串的起始位置和结束位置。
bool checkPartitioning(string s)
{
//1,用动态规划思想把所有子串是否是回文串预处理一下
int n = s.size();
vector> dp(n,vector (n));
for(int i = n-1; i >= 0; i--)
for(int j = i; j < n; j++)
if(s[i] == s[j])
dp[i][j] = i+1 < j ? dp[i+1][j-1] : true;
//2.枚举所有的第二个字符串的起始位置以及结束位置
for(int i = 1; i < n; 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;
}
132. 分割回文串 II - 力扣(LeetCode)
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是回文串。返回符合要求的 最少分割次数 。
示例 1:
输入:s = "aab" 输出:1 解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
示例 2:
输入:s = "a" 输出:0
1.状态表示
dp[i]: 表示字符串s在区间(0,i)上的最长子串,最少分割次数
2.状态转移方程
dp[i]位置的值可以分类讨论:
如果是回文,则由dp[j-1]+1,在所有分割中取次数最少分割,dp[i] = min(dp[j-1]+1,dp[i])
如果不是回文则,继续枚举下一个区间
优化措施:
用二维dp表,将所有的子串是否是回文的信息保存在dp表中
3.初始化
首先,由状态转移方程可以知道只有当j = 0时才可能发生越界访问情况,但是j 的取值范围是
0 < j <= i,
其次,为了防止第一次求min时,dp[i]的值干扰结果,所以将dp表中所有的值初始化为无穷大
4.填表顺序
从左向右依次填写
5.返回值
dp[n-1]
int minCut(string s)
{
//1.预处理所有子串是否是回文子串、
int n = s.size();
vector> dp(n,vector(n));
for(int i = n-1; i >= 0; i--)
for(int j = i; j < n; j++)
if(s[i] == s[j])
dp[i][j] = i+1 < j ? dp[i+1][j-1] : true;
vector dp2(n,INT_MAX);
for(int i = 0; i < n; i++)
{
if(dp[0][i]) dp2[i] = 0;
else
{
for(int j =1; j <= i; j++)
{
if(dp[j][i])
dp2[i] = min(dp2[i], dp2[j-1]+1);
}
}
}
return dp2[n-1];
}
516. 最长回文子序列 - 力扣(LeetCode)
给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab" 输出:4 解释:一个可能的最长回文子序列为 "bbbb" 。
示例 2:
输入:s = "cbbd" 输出:2 解释:一个可能的最长回文子序列为 "bb" 。
1.状态表示
dp[i][j]:表示字符串s在区间(i,j)内所有子序列中,最长的回文序列长度
2.状态转移方程
对于dp[i][j]的值作如下分类讨论:
(1)s[i] == s[j],根据子串长度做如下分类讨论:
(2) s[i] != s[j] , 则dp[i][j] = max(dp[i+1][j],dp[i][j-1])
3.初始化
由状态转移方程可以知道,对于dp[i][j]的值可能需要用到 dp[i+1][j-1]、dp[i+1][j]、dp[i][j-1]三个位置的值,只有dp[0][0]和dp[n-1][n-1]可能发生越界访问,但是在分类讨论时已经处理过,所以不需要进行初始化
4.填表顺序
从下往上填,每一行从左往右填
5.返回值
dp[0][n-1]
int longestPalindromeSubseq(string s)
{
int n = s.size();
vector> dp(n,vector(n));
for(int i = n-1; i >= 0; i--)
{
dp[i][i] = 1;
for(int j = i+1; j < n; j++)
{
if(s[i] == s[j]) dp[i][j] = dp[i+1][j-1] +2;
else dp[i][j] = max(dp[i+1][j],dp[i][j-1]);
}
}
return dp[0][n-1];
}
动态规划算法为解决回文串问题提供了一种系统性的方法。通过合理定义状态和状态转移方程,我们能够有效地找出最长回文子串。理解这一过程不仅有助于解决具体的回文串问题,还能提升对动态规划算法的理解和运用能力,在面对其他类似的字符串处理或最优子结构问题时,能够更得心应手地运用动态规划思想找到解决方案。希望本文的讲解能帮助你在算法学习的道路上更上一层楼,对动态规划与回文串问题有更深入的认识。