[dp12_回文子串] 最长回文子串 | 分割回文串 IV
目录
1.回文子串
题解
2.最长回文子串
题解
3.分割回文串 IV
题解
dp[i][j] 表示 s 字符串 [i, j] 的子串,是否是回文串( 建始末表)
- 将两个 for 循环的结果,借助二维 dp 来存
1.回文子串
链接:647. 回文子串
给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
示例 1:
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
子串和子数组是一样的。
- 具有不同开始位置或者结束位置的子串
- 即使是由相同的字符组成,也会被视作不同的子串。
题解
对于这道题相比于动态规划,中心扩展算法和马拉车算法更优秀。
- 中心扩展算法:时间复杂度O(N^2),空间复杂度O(1)
马拉车算法:时间复杂度O(N),空间复杂度O(N)
动态规划:时间复杂度O(N^2), 空间复杂度O(N^2)
动态规划虽然这里不是最优解,但是它的思想可以让有些回文串的困难题变成容易题。
能够将所有的子串是否是回文的信息,保存在 dp 表里面
1.状态表示
既然要把所有子串都放在dp表里面,一个一维dp肯定不够
-
dp[i][j] 表示 s 字符串 [i, j] 的子串,是否是回文串
2.状态转移方程
- 判断 i - > j 是否是回文串,必须首先 i 位置 和 j 位置字符是相等的,如果都不相等,即使里面在好, i - > j 子串也不回文串
- i 位置 和 j 位置字符相等,这里分三种情况:
i == j , i j同一个位置 是回文子串
i + 1 == j ,i j相邻 是回文子串
还有就是 i -> j 中间有别的字符,那就看 i + 1 位置 到 j - 1 位置的子串是否是回文子串,而i + 1 位 到 j - 1 是否是回文子串就在dp[i + 1][j - 1]
3.初始化
- 会发生越界的就只有 dp[i + 1][j -1]
- 但是它会发生越界情况都已经在 i == j,i + 1 < j 这两种情况考虑到了。
- (以二维表的 视角 来看一下就懂啦)
4.填表顺序
- dp[i][j] 可能会用到 dp[i + 1][j -1]
- 所以是从左下往右上填的
5.返回值
- 统计dp表中true的个数。
class Solution {
public:
int countSubstrings(string s)
{
int n=s.size();
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;
if(s[i]==s[j])
{
if(i==j)
dp[i][j]=true;//重合
else if(i+1==j)
dp[i][j]=true;//相邻
else
dp[i][j]=dp[i+1][j-1];
//不存在越界 因为重合和相邻的情况
//已经给这个二维数组 填好了一部分
}
}
}
int ret=0;
for(int i=0;i<n;i++)
{
for(int j=i;j<n;j++)
{
if(dp[i][j])
ret++;
}
}
return ret;
}
};
中心扩展算法
前文:[Lc11_字符串] 最长公共前缀 | 最长回文子串 | 二进制求和
class Solution {
public:
string longestPalindrome(string s)
{
int n=s.size();
int len=0;
string ret;
for(int i=0;i<n;i++)
{
int left=i-1,right=i+1;//ji
while(left>-1 && right<n &&s[left]==s[right])
{
left--;
right++;
}
if(right-left-1>ret.size())
//!!!!(pos,len) not(pos,end)
ret=s.substr(left+1,right-left-1);
left=i,right=i+1;//ou
while(left>-1 && right<n &&s[left]==s[right])
{
left--;
right++;
}
if(right-left-1>ret.size())
ret=s.substr(left+1,right-left-1);
}
return ret;
}
};
2.最长回文子串
链接: 5. 最长回文子串
给你一个字符串 s
,找到 s
中最长的 回文 子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
题解
- 如果上面那道题搞懂了,这道题完全直接就去做就行了。可以这样做:
- 判断子串是否是回文 —> 用 dp 表,统计所有子串是否是回文的信息 —> 根据 dp 表的起始和结尾位置得到我们想要的结果。
起始位置 i,回文串结束位置 j,回文串长度 j - 1 + 1
1.状态表示
- dp[i][j] 表示 s 字符串 [i, j] 的子串,是否是回文串
2.状态转移方程
3.初始化
- 无需初始化,当用到 dp[i+1][j-1] 是有条件的
- 当 i + 1 < j才会用,此时不会越界。(确保 是 中间有别人的时候)
- 只有当 i == j(重合),i + 1 == j(相邻) 才会越界,但是我们已经特殊处理过了。
4.填表顺序
- dp[i][j] 可能会用到 dp[i + 1][j -1],所以从下往上填。
- 其实当成子串的坐标来理解 i j 就好啦
5.返回值
- dp 里面值为 true 的情况下,长度最大的子串的起始位置以及长度
class Solution {
public:
string longestPalindrome(string s)
{
int n=s.size();
vector<vector<bool>> dp(n,vector<bool>(n,false));
for(int i=n-1;i>=0;i--)
//根据DP方向
//用的是i+1,依赖的后一位置
//所以是从后往前填
{
for(int j=i;j<n;j++)
{
if(s[i]==s[j])
{
if(i==j)
dp[i][j]=true;
else if(i+1==j)
dp[i][j]=true;
else
dp[i][j]=dp[i+1][j-1];
}
}
}
int len=0,begin=0;
for(int i=0;i<n;i++)
{
for(int j=i;j<n;j++)
{
if(dp[i][j])
{
int cur=j-i+1;
if(cur>len)
{
len=cur;
begin=i;
}
}
}
}
return s.substr(begin,len);
}
};
3.分割回文串 IV
链接: 1745. 分割回文串 IV
给你一个字符串 s
,如果可以将它分割成三个 非空 回文子字符串,那么返回 true
,否则返回 false
。
当一个字符串正着读和反着读是一模一样的,就称其为 回文字符串 。
示例 1:
输入:s = "abcbdd"
输出:true
解释:"abcbdd" = "a" + "bcb" + "dd",三个子字符串都是回文的。
示例 2:
输入:s = "bcbddxy"
输出:false
解释:s 没办法被分割成 3 个回文子字符串。
给你一个字符串 s,把它分成三个部分,如果这三个部分全都是回文子串,返回true,否则false。
题解
我们先想如何暴力解决这个问题,我们可以把所有的分割出来的三个子串枚举一下,然后判断是否是回文就可以了。
- 我们可以这样枚举,固定一个 i 位置,一个 j 位置,i j 表示的是分割出来的第二个字符串的起始位置和结束位置
- 那整个字符串,就被分成 [0, i -1],[i, j], [j+1, n-1] 三个部分。
如果我们能够快速判断这三个子串是否是回文就好了。
- 看到快速判断,是不是就想到了 我们 dp 表存的起始和结束位置,就可以快速判断
别忘记,回文子串,我们可以用 dp 表保存所有子串是否是回文的信息。
- 也就是说用动态规划,搞一个二维的dp表,保存所有子串是否是回文的信息
- 接下来在枚举所有第二个字符串的起始位置和结束位置
- 仅需在dp表里面查一下三个部分是否是回文就行了。
将三层循环,借助 dp 的状态查询,两层循环就够了
class Solution {
public:
bool checkPartitioning(string s)
{
int n=s.size();
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])
{
if(i==j)
dp[i][j]=true;
else if(i+1==j)
dp[i][j]=true;
else
dp[i][j]=dp[i+1][j-1];
}
}
}
//双指针 划分
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;
}
};