今天继续学习动规解决子序列问题。
647.回文子串
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
思路:
1.在结束了前面的子序列问题后迎来了最后关于回文子序列的问题,对于回文串,我们首先肯定会想到暴力双层for循环遍历,而要通过动规解决的话得考虑当前回文串能够通过之前的什么情况推断出来。在前者回文串的基础之上判断当前是否是回文串,无疑就是判断当前字符的首尾字符是否相等,然后再看原本的字符串部分是否是回文串。
2.首先想dp数组含义,布尔数组dp[i][j]表示[i,j]区间的字符是否是回文串。
3.然后想递推公式,如果s[i] != s[j],那么显然dp[i][j] = false。而如果s[i] == s[j],我们就稍微分一下情况,如果j - i <= 1,此时要么i和j相等,对应单个字符;要么i和j相邻,如示例中的aa,这种情况下我们是不需要判断之前的字符串是否是回文串的情况的。如果j - i > 1,那么我们就需要判断之前的字符串是否是回文串了,即dp[i + 1][j - 1],如果dp[i + 1][j - 1]也为true那么dp[i][j]就为true。
4.然后想初始化,由递推公式可以看出,如果一个字符串为true,那么对于后续判断也是有影响的,而对于单个字符实际上我们递推公式已经考虑到了所有情况,因此不需要特别将所有i == j的情况初始化为true,直接全部初始化为false即可。
5.最后想遍历顺序。将i,j具体化为一个二维矩阵,i表示行,j表示列,通过递推公式我们不难看出dp[i][j]是由其斜左下方的元素推导出来的,因此遍历顺序为从下往上,从左往右。
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
int result = 0;
for(int i = s.size() - 1; i >= 0; i--){
for(int j = i; j < s.size(); j++){
if(s[i] == s[j]){
if(j - i <= 1){
dp[i][j] = true;
result++;
}
else if(dp[i + 1][j - 1]){
dp[i][j] = true;
result++;
}
}
else{
dp[i][j] = false;
}
}
}
return result;
}
};
启发:
1.本题是第一次接触用动规解决回文串问题,但有了前面的子序列问题基础后,本题思路还是比较容易想出来的。主要还是递推公式部分的判断,以及本题的遍历顺序实际上是小有变化了的,最好画出i和j的二维矩阵进行判断遍历顺序。
516.最长回文子序列
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
示例 2:
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。
思路:
1.本题相比于上一题最大的不同就在于找的是子序列了,因此不需要中间是连续的。
2.首先想dp数组含义,dp[i][j]表示[i,j]区间内最长回文子序列长度。
3.然后想递推公式,主要还是通过s[i]和s[j]是否相等来划分情况。如果相等,那么就在[i + 1, j - 1]的最长回文子序列长度之上加2,即dp[i][j] = dp[i + 1][j - 1] + 2;如果不相等,那么我们就分开单独考虑将两个字符中的一个纳入考虑范围,最后取最大值,即dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])。
4.然后想初始化,本题由递推公式dp[i][j] = dp[i + 1][j - 1] + 2可以看出无法在递推过程中推出i,j相等的情况,因此这部分我们需要初始化。而i与j相等无非就是单个字符,显然是满足回文子序列的,因此我们将其初始化为1.
5.最后想遍历顺序,同样的在二维矩阵中结合递推公式不难看出,dp[i][j]可能由其左方,下方,斜左下方推导出来。因此遍历顺序是从下往上,从左向右。
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for(int i = 0; i < s.size(); i++){
dp[i][i] = 1;
}
for(int i = s.size(); i >= 0; i--){
for(int j = i + 1; j < s.size(); 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][s.size() - 1];
}
};
启发:
1.本题因为是回文子序列,不要求连续,因此我们就没必要还要考虑[i + 1, j - 1]部分的字符串是不是回文串了,只要求长度就可。
2.对于i和j的遍历顺序是否可颠倒,显然是不可以的,因为区间设置的是[i,j]那么j显然是依赖于i确定的(因为得大于等于i)。