对于回文子串,回文表示,一个字符串是镜像的,也就是说这个字符串从前向后与从后向前对应的字符是相同的。
回文子串,表示的是镜像的连续子串。
对此,我们可以用动态规划的思路去解决这一道题目。
用dp数组记录回文子串的数量。dp[ii]表示截止到ii所包含的回文子串数量。其值等于截止到ii-1所包含的回文子串数量加上以ii结尾的回文子串数量。因此,我们可以写一个函数来判断一个字符串是不是回文的。之后只要从前向后遍历到ii判断这个字符串是否回文即可。
我们要判断一个子串是否回文,我们只需一直对比首尾元素即可。如果存在一个元素不相同,就返回false,否则返回true。
bool huiwen(string &s,int start,int end)
{
while(start<end)
{
if(s[start]!=s[end]) return false;
start++;
end--;
}
return true;
}
动态规划四部曲。
1.dp数组的含义。dp[ii]表示截止到ii所包含的回文子串数量。
2.dp数组的推导公式。dp[ii]等于截止到ii-1所包含的回文子串数量加上以ii结尾的回文子串数量。
int temp=0;
for(int jj =0;jj<=ii;jj++)
if(huiwen(s,jj,ii))
temp++;
dp[ii] = dp[ii-1]+temp;
3.dp数组的初始化。因为单独一个首元素也是一个回文子串,因此,我们令首元素尾1即dp[0]=1
,
4.dp数组的遍历顺序,我们可以从前向后遍历。
class Solution {
public:
bool huiwen(string &s,int start,int end)
{
while(start<end)
{
if(s[start]!=s[end]) return false;
start++;
end--;
}
return true;
}
int countSubstrings(string s) {
vector<int>dp(s.size(),1);
if(dp.size()<=1) return dp.size();
for(int ii =1;ii<s.size();ii++)
{
int temp=0;
for(int jj =0;jj<=ii;jj++)
if(huiwen(s,jj,ii))
temp++;
dp[ii] = dp[ii-1]+temp;
}
return dp[s.size()-1];
}
};
上述暴力解法,时间复杂度尾O(n^3),我们可以用空间换取时间。
如果我们提前知道s[ii+1]到s[jj-1]是回文的,那么只要s[ii]和s[jj]相等,那么从s[ii]到s[jj]就也是回文的,因此我们可以用dp数组来实现这样的记录。
定义dp[ii][jj]为bool类型的二维数组,用来记录从ii到jj是否是回文的。对于dp[ii][jj],有四种情况,
如果s[ii]!=s[jj]那么我们直接让dp[ii][jj]=false,既然头与尾都不相等,那么一定不是回文的。
如果s[ii]==s[jj],并且ii=jj,即这个字符串只有一个字符那么他一定是回文的。
如果s[ii]==s[jj],并且两个元素紧邻,相邻的两个相同元素也一定是回文的。
如果s[ii]==s[jj],但是这个字符串的长度大于1,那么我们需要dp[ii+1][jj]的信息,如果dp[ii+1][jj]==true,那么说明从ii+1到jj-1是回文的,那么从ii到jj也是回文的。如果dp[ii+1][jj]==false,那么就代表着中间元素不是回文的,原字符串也不是回文的。
在第三种情况中,我们使用到了下一行的信息,因此,我们不能从第一行往后遍历,因为后面信息没有更新的话,会导致前面的元素不准确。
因此,我们需要从最后一行进行遍历,每一行从前向后遍历。
并且在遍历过程中,每遇到一次回文子串,我们都让结果进行加1,这样最后就不用再额外统计回文子串个数了。
动态规划四部曲。
1.dp数组含义。用来记录从ii到jj是否是回文的
2.dp数组的推导公式。
if(s[jj] == s[ii])
{
if((jj-ii)<=1)
{
dp[ii][jj] =true;
result++;
}
else
{
if(dp[ii+1][jj-1])
{
dp[ii][jj] = true;
result++;
}
}
}
3.dp数组的初始化,显然,我们没有记录false的情况,因此都初始化为false即可。
4.遍历顺序,我们对每个首元素从后向前进行遍历,尾元素从前向后进行遍历。
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>>dp(s.size(),vector<bool>(s.size(),false));
int result = 0;
for(int ii =s.size()-1;ii>=0;ii--)
{
for(int jj = ii;jj<s.size();jj++)
{
if(s[jj] == s[ii])
{
if((jj-ii)<=1)
{
dp[ii][jj] =true;
result++;
}
else
{
if(dp[ii+1][jj-1])
{
dp[ii][jj] = true;
result++;
}
}
}
}
}
return result;
}
};
与上一题不一样的是,本题求得是子序列,也就是说所要求的子串不一定要连续。
因此,我们需要修改一下思路。
相对于连续子串,如果是子序列的话,我们一般是定义dp[ii][jj]为以s[ii]开始以s[jj]为结尾…。
对于此题,我们就定义dp[ii][jj]为以s[ii]开始以s[jj]为结尾,存在的最长回文子序列。
因此,如果首元素与尾元素相等,
1.如果ii==jj,那么相当于只有一个字符,对此,我们让这个dp[ii][jj] = 1;
2.如果ii == jj+1,那么说明有两个字符,我们让dp[ii][jj] = 2;
3.如果ii-jj>1,那么说明有两个以上的字符,因此,如果去掉首元素和尾元素,相当于最长回文子序列的个数就变成了dp[ii+1][jj-1],于是我们只需要在这个基础加上首尾元素即可。dp[ii][jj] = dp[ii + 1][jj - 1] + 2;
但是如果首元素和尾元素不相等,就相当于,如果我们要找最长的回文子序列,我们只能从舍弃首元素或者舍弃尾元素中取最大的,也就是说
dp[ii][jj] = max(dp[ii+1][jj],max(dp[ii][jj - 1],dp[ii+1][jj-1]));
最后我们返回dp[0][dp[0].size()-1]即可,因为这个元素表示从下标0到下标最大值所包含的最长回文子序列。
动态规划四部曲。
1.dp数组的含义。dp[ii][jj]为以s[ii]开始以s[jj]为结尾,存在的最长回文子序列。
2.dp数组的推导公式。
if (s[ii] == s[jj])
{
if ((jj - ii) == 0)
{
dp[ii][jj]=1;
}
else if (jj == ii + 1)
{
dp[ii][jj]=2;
}
else
{
dp[ii][jj] = dp[ii + 1][jj - 1] + 2;
}
}
else
dp[ii][jj] = max(dp[ii+1][jj],max(dp[ii][jj - 1],dp[ii+1][jj-1]));
3.dp数组的初始化。显然,我们可以初始化这个数组为0,但是无所谓,我们遍历元素时,如果只有一个字符,我们直接令其为1,如果有两个字符,我们令其为2,因此没有影响。
4.dp数组的遍历顺序,首元素从尾开始遍历,尾元素从前向后进行遍历。
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>>dp(s.size(), vector<int>(s.size(), 0));
int result = 0;
for (int ii = s.size() - 1; ii >= 0; ii--)
{
for (int jj = ii; jj < s.size(); jj++)
{
if (s[ii] == s[jj])
{
if ((jj - ii) == 0)
{
dp[ii][jj]=1;
}
else if (jj == ii + 1)
{
dp[ii][jj]=2;
}
else
{
dp[ii][jj] = dp[ii + 1][jj - 1] + 2;
}
}
else
dp[ii][jj] = max(dp[ii+1][jj],max(dp[ii][jj - 1],dp[ii+1][jj-1]));
}
}
return dp[0][dp[0].size()-1];
}
};