欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 养生 > 动态规划学习——回文子串系列问题【C++】

动态规划学习——回文子串系列问题【C++】

2025/4/3 11:25:02 来源:https://blog.csdn.net/2401_82677021/article/details/146958443  浏览:    关键词:动态规划学习——回文子串系列问题【C++】

一,回文子串

题目链接:LCR 020. 回文子串 - 力扣(LeetCode)

 【问题描述】

求一个字符串中有多少个回文子串,其中一个字符也算是一个回文子串。

【解法】

动态规划

求一个字符串中回文子串的个数,我么可以找到每个回文子串,然后统计个数即可。

首先定义状态表示:

dp[i]:表示子串[i-j],是否是回文串。其中下标i<=j。


状态转移方程的推导:

当s[i]!=s[j]时,也就是子串的第一个字符和最后一个字符不相等,那么肯定不是回文串,所以dp[i][j]=false。

当s[i]==s[j]时,在该情况下,又细分三种情况:

  • 如果i==j,那么该子串就是一个字符,是回文串,所以dp[i][j]=true。
  • 如果i+1==j,也就是该字符串由两个字符构成,且s[i]==s[j],是回文串,所以dp[i][j]=true。
  • 如果i+1<j,表示s[i]和s[j]中还有一些字符,那么dp[i][j]=dp[i+1][j-1]。

初始化dp表时,根据状态表示的定义,我们只会用到dp表主对角线的右上部分,左下部分不会用到,对于状态转移dp[i][j]=dp[i+1][j-1],我们不需要考虑越界的问题,因为前两种情况已经做了判断

 最后,只需统计dp表中dp[i][j]==true的数量即可。

【代码】

class Solution {
public:int countSubstrings(string s) {//dp[i]:子串[i-j]是否是回文串int n=s.size();int sum=0;//统计结果vector<vector<bool>> dp(n,vector<bool>(n,false));for(int i=n-1;i>=0;i--)for(int j=i;j<n;j++){if(s[i]!=s[j]){dp[i][j]=false;}else{dp[i][j]=i+1<j?dp[i+1][j-1]:true;}if(dp[i][j])sum++;}return sum;}
};

时间复杂度O(N^2),空间复杂度O(N^2) 

二,最长回文子串

题目链接:5. 最长回文子串 - 力扣(LeetCode)

【题目描述】

 求字符串中最长的回文子串。

【解法1】

动态规划

上一题是求回文子串的个数,我们用一个dp表来表示【i-j】是否是回文串。

本题可以直接使用上题 的思路,如果dp[i][j]是回文串,也就是dp[i][j]==true,那么看看该子串的长度是否是最大的,该子串的长度是j-i+1。可以使用一个变量len来记录最长的回文子串的长度,begin来记录该回文串的开始位置i。最后使用substr就可以获取到该子串。

【代码】

class Solution {
public:string longestPalindrome(string s) {int n=s.size();vector<vector<bool>> dp(n,vector<bool>(n));int len=0,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]){if(len<j-i+1){len=j-i+1;begin=i;}}}return s.substr(begin,len);}
};

时间复杂度:O(N^2),空间复杂度O(N^2)

【解法2】

中心扩展算法

求最长的回文子串。我们可以从一个字符出发,求出它作为中心所能形成的最长回文串的长度。

用一个变量i来记录遍历到字符串的哪一个为位置。

每遍历到一个位置,向两边扩展,用两个变量left和right来记录扩展位置的下标。

回文子串的长度可能是奇数,也可能是偶数,所以需要计算两次:

1,回文子串的长度是奇数,让left=i-1,right=i+1,向两边扩展,如果s[left]==s[right],

left--,right++。最后回文子串的长度就是right-left-1。

2,回文子串的长度是偶数,让left=i-1,right=i(left=i,right=i+1也可以),同理,如果s[left]==s[right],left--,right++。最后回文子串的长度就是right-left-1。

记录这两种情况下回文子串最长的长度,以及该回文子串开始的下标。

【代码】

class Solution {
public:string longestPalindrome(string s) {int n=s.size();int begin=0,len=0;for(int i=0;i<n;i++){//奇数扩展int left=i,right=i;while(left>=0&&right<n&&s[left]==s[right]){left--;right++;}if(len<right-left-1){len=right-left-1;begin=left+1;}//偶数扩展left=i-1,right=i;while(left>=0&&right<n&&s[left]==s[right]){left--;right++;}if(len<right-left-1){len=right-left-1;begin=left+1;}}return s.substr(begin,len);}
};

时间复杂度O(N),空间复杂度O(1) 

三,回文串分割IV

题目链接:1745. 分割回文串 IV - 力扣(LeetCode)

 【题目描述】

对于一个字符串,将他分为任意的三个子串,如果存在这三个子串都是回文串,返回true,否则返回false

【解法】

将一个字符串分割成三个子串,可以想象成在一个字符串中,切两刀,分成三部分,而这两“刀”就是下标,我们用下标i,j将字符串分成三个部分,[0,i-1],[i,j],[j+1,n-1],n为字符串长度。

所以只需判断这三个子串是否是回文串即可。而在第一道题中我们使用一个dp表来表示[i-j]子串是否是回文串。所以,在本题中,我们可以使用dp表来做预处理,然后枚举这三个子串:

[0,i-1],[i,j],[j,n-1],判断这三个子串是否是回文串。

【代码】

class Solution {
public:bool checkPartitioning(string s) {//dp表存储回文信息【i,j】int n=s.size();vector<vector<bool>> dp(n,vector<bool>(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;}}for(int i=0;i<n;i++)for(int j=0;j<n;j++){if(dp[i][j]&&i-1>=0&&j+1<n)if(dp[0][i-1]&&dp[j+1][n-1])return true;}return false;}
};

四,回文串分割II

题目链接:LCR 094. 分割回文串 II - 力扣(LeetCode)

【解法】

动态规划

首先定义状态表示:

dp[i]:表示【0-i】区间的子串,最少的切割次数。

状态转移方程的推导:

如果子串【0-i】本身就是回文串,就不需要切割,即dp[i]=0.

如果子串【0-i】不是回文串,需要切割,我们可以枚举切割的位置j,0<j<=i。其中不用枚举j=0,因为j=0,也就是【0-i】区间。j从i开始枚举,接下来判断【j-i】是否是回文串:

  • 如果【j-i】是回文串,那么就找到了一个分割点,dp[i]就等于【0,j-1】区间的最少分割次数+1,
  • 即dp[i]=dp[j-1]+1。
  • 如果【j-i】不是回文串,就不是一个分割点,继续找分割点。

其中求的是最少的分割次数,所以

dp[i]=min(dp[i],dp[j-1]+1)。

上面的过程 中,会一直判断某一段区间是否是回文串,所以可以用第一道题的思路,用一张表来记录区间【i-j】是否是回文串。

初始化dp表:

不用判断数组越界的问题,因为j是大于0的。

由于求的是最小值,所以不能将数组的初始值设为0,会影响最终结果。需要设为无穷大。

【代码】

class Solution {
public:int minCut(string s) {int n=s.size();//i-j区间是否是回文串vector<vector<bool>> isPal(n,vector<bool>(n,false));for(int i=n-1;i>=0;i--)for(int j=i;j<n;j++){if(s[i]!=s[j]){isPal[i][j]=false;}else{isPal[i][j]=i+1<j?isPal[i+1][j-1]:true;}}//0-i区间最少分割次数vector<int> dp(n,INT_MAX);for(int i=0;i<n;i++){//0-i是回文串if(isPal[0][i]){dp[i]=0;continue;}//枚举分割点,求最少分割次数for(int j=i;j>0;j--){//j-i是回文串if(isPal[j][i]){dp[i]=min(dp[i],dp[j-1]+1);}}}return dp[n-1];}
};

 时间复杂度O(N^2),空间复杂度O(N^2)

五,最长回文子序列

题目链接:516. 最长回文子序列 - 力扣(LeetCode)

【题目描述】

本题与上面的题不同,本题是求最长的回文子序列的长度,子序列是指元素可以不连续,而前面的子串问题,都是要求元素是连续的。

 【解法】

按照常规的思路,我们会定义如下的状态方程:
dp[i]:表示以第i个元素为结尾的所有子序列中,最长的回文子序列的长度。

要想推导dp[i],一定会与dp[i-1],dp[i-2],dp[i-3]......有关,因为是子序列问题,第i个字符可以拼接到第i-1个字符的后面,也可以拼接到第i个字符的后面......所以会需要前面的dp值来更新dp[i]的值。比如需要通过dp[i-1]来推导dp[i]的值,但是dp[i-1]表示的以第i-1个元素为结尾的最长回文子序列的长度,我们将第i个元素拼接到第i-1个元素的后面,无法判断拼接后的子序列是否是回文串,所以无法推导,所以这样的状态表示是不行的。


在“回文子串”题中,我们判断一段区间【i-j】是否是回文串,对于这段区间,我们只关心两个端点的状态,也就是s[i]和s[j]的关系。本题也类似。

首先定义状态标识:

dp[i][j]:表示【i-j】区间中最长的回文子序列的长度。

状态转移方程的推导:

【i     i+1       j-1    j】    

如果s[i]==s[j],有三种情况:

  • i==j,表示一个字符,是回文串,长度为1,所以dp[i]=1。
  • i+1==j,表示两个 字符相邻并且相等,那么回文串的长度就是2,即dp[i]=2。
  • i+1<j,表示i和j之间还有其他字符,我们需要求出区间【i+1,j-1】的最长回文子序列的长度,然后再在前面和后面分别拼上s[i]和s[j]即可。所以dp[i]=dp[i+1][j-1]+2。

如果s[i]!=s[j]:

从整体上来看这整个区间【i-j】,【i-j】的回文子序列包含两部分:

【i,j-1】的回文子序列和【i+1,j】的子序列。所以dp[i][j]=max(dp[i][j-1],dp[i+1][j])。

初始化dp表示:

只需关心dp[i][j]=max(dp[i][j-1],dp[i+1][j])这个表达式中的每一项是否会越界。

前提条件还是i<j,也就是我们只会用到dp表主对角线的右上部分。

而dp[i][j]的更新会用到左边和下边的值:

可以发现,这两个位置其实是主对角线上的元素,满足i==j,而这种情况我们在前面已经判断过了,所以不需要初始化。

 

class Solution {
public:int longestPalindromeSubseq(string s) {int n=s.size();//i-j区间的最长回文子序列vector<vector<int>> dp(n,vector<int>(n,0));for(int i=n-1;i>=0;i--)for(int j=i;j<n;j++){if(s[i]==s[j]){if(i==j)dp[i][j]=1;else if(i+1==j)dp[i][j]=2;elsedp[i][j]=dp[i+1][j-1]+2;}else{dp[i][j]=max(dp[i][j-1],dp[i+1][j]);}}return dp[0][n-1];}
};

版权声明:

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

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

热搜词