● 647. 回文子串
1.dp数组含义。
之前的题目,差不多都是求什么就怎么定义dp数组,最后返回dp的最后一个元素。但是这里如果定义一维数组dp[i]是[0,i]范围的回文子串的个数的话,怎么根据dp[i-1]得到dp[i]?发现很难找到递归关系,回文串需要固定两端来讨论判断。
所以需要二维数组。又:dp数组不统计个数,而是判断是否是回文子串的话,比较好推导递归关系:因为根据回文子串的定义,如果下标1、2、3组成的子串是回文串,那么只需要下标0和4的字符相等,就可以判定为回文子串。而统计个数的话还是需要比较中间所有的元素。
所以dp[i][j]:下标范围为[i,j]的子串是否回文,是为true,否为false。i、j都是闭区间,当i=j的时候就是单个字符,好初始化。
最后返回dp数组中值为true的个数。
2.递推公式。
参照上图,如果s[i]==s[j]而且中间的子串[i+1,j-1]是回文子串的话,那么[i,j]子串肯定是回文子串。这个图只考虑了i和j中间至少一个元素的情况,实际上在相等的条件下根据i和j的大小,得到dp[i][j]有3种情况:
①i==j:就一个字符,[i,j]是回文子串。
②j==i+1:2个字符,只要满足一个条件:s[i]==s[j],就是一个回文子串。
③j>i+1:有≥2个字符,只要满足相等和dp[i+1][j-1]=true这两个条件的话,[i,j]就是回文子串。
所以dp[i][j]在上面3种情况中=true,其他的都是false,可以直接在初始化的时候设定。因为这里的条件且和或 的逻辑有点多,所以就分开写:
if(s[i]==s[j]){
if(j<=(i+1))//情况1和2
{
dp[i][j]=true;
count++;
}
if(i<n-1&&j>0&&dp[i+1][j-1]){//情况3
dp[i][j]=true;
count++;
}
}
3.初始化。
在一开始定义dp数组的时候,我们就所有元素都初始化为false,到递推的时候再把每个元素更新为true。
注意情况①没有在初始化时设定,本来这个递推比较耗时间,在循环前面初始化的话有2个例子会超时。
4.遍历顺序。
这题的遍历顺序踩坑了,其实这个dp数组是一个对称矩阵,我们只需要统计对角线上true的个数加上:左下角或者右上角的true个数即可。再看定义的时候,我们说范围[i,j]内的,所以定为i<=j,即统计的是对角线+右上角的。又因为dp[i][j]是否为true取决于dp[i+1][j-1],[i+1,j-1]是在[i,j]的左下角,说明到达i、j的时候,左下角的dp是需要更新了的。所以在i<=j的前提下:
(1)先列后行(先j后i):
for(int j=0;j<n;++j){
for(int i=0;i<=j;++i){
}}
(2)先行后列(先i后j)
for(int i=n-1;i>=0;--i){
for(int j=i;j<n;++j){
}}
告诉我们遍历顺序需要简画一下dp数组的结构图。
5.打印。
代码如下。列优先的情况需要增加条件i<n-1 && j>0,行优先的话不需要添加。
class Solution {
public:
int countSubstrings(string s) {
int n=s.size();
int count=0;
vector<vector<bool>> dp(n,vector<bool>(n,false));
for(int j=0;j<n;++j){
for(int i=0;i<=j;++i){
if(s[i]==s[j]){
if(j<=(i+1))
{
dp[i][j]=true;
count++;
}
if(i<n-1&&j>0&&dp[i+1][j-1]){
dp[i][j]=true;
count++;
}
}
cout<<dp[i][j]<<" ";
}
}
return count;
}
};
● 516.最长回文子序列
区别:回文子串:连续;回文子序列:非连续。
1.dp数组含义。
dp[i][j]:s[i,j]范围内回文子序列的最大长度。
2.递推公式。
(1)如果s[i]==s[j],说明[i+1,j-1]中的最长回文子序列加上这两个也构成回文子序列,所以:
dp[i][j]=dp[i+1][j-1]+2;
①i==j:需要写出来dp[i][j]=1。
②i+1<=j:+1==j的时候dp[i][j]=2。不用分开写出,因为dp[i+1][j-1]是左下角的元素,左下角都为0而且不会更新,所以<=的时候都可以用dp[i][j]=dp[i+1][j-1]+2;得到。
(2)如果如果s[i]!=s[j],不像上一道题每个元素不是true就是false。不相等的话,[i,j]的长度也需要求出。那么最长回文子序列要么不包含s[i],要么不包含s[j],所以有两种情况:不考虑s[i]或不考虑s[j]。取两个的最大值即可:dp[i][j]=max(dp[i][j-1],dp[i+1][j]);
所以递推如下,与上题一样条件有点多,分开写更清晰:
if(j==i)dp[i][j]=1; //情况(1)①
else if(i<n-1 && j>0){
if(s[i]==s[j])dp[i][j]=dp[i+1][j-1]+2; //情况(1)②
else dp[i][j]=max(dp[i][j-1],dp[i+1][j]);//情况(2)
}
3.初始化。
一开始全部初始化为0即可。左下角初始化为0,也为[i,j]内只有2个元素的递推做准备。
4.遍历顺序。和上题一样,根据不相等和相等的递推公式,注意dp[i][j]的3个决定元素在哪个位置。递推顺序必须满足这个先后次序。
可见dp[i][j]的左下角、左边以及下边的元素需要先于dp[i][j]更新,所以先列后行的话,j从0开始,i从j开始。
for(int j=0;j<n;++j){
for(int i=j;i>=0;--i){
}}
先行后列的话,i从n-1开始,j从i开始。
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
}}
5.打印。
先列后行,代码如下:
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n=s.size();
vector<vector<int>> dp(n,vector<int>(n,0));
for(int j=0;j<n;++j){
for(int i=j;i>=0;--i){
if(j==i)dp[i][j]=1;
else if(i<n-1 && j>0){
if(s[i]==s[j])dp[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];
}
};
这两道题感觉体现了之前背包问题里面遍历顺序的重要性。
● 动态规划总结篇
动规基础:
背包问题:
打家劫舍问题:
股票问题:
子序列问题:
动规五部曲贯穿这几十道动规问题:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
摘自代码随想录:
动规五部曲里,哪一部没想清楚,这道题目基本就做不出来,即使做出来了也没有想清楚,而是朦朦胧胧的就把题目过了。
- 如果想不清楚dp数组的具体含义,递归公式从何谈起,甚至初始化的时候就写错了。
- 例如动态规划:不同路径还不够,要有障碍!
- (opens new window) 在这道题目中,初始化才是重头戏
- 如果看过背包系列,特别是完全背包,那么两层for循环先后顺序绝对可以搞懵很多人,反而递归公式是简单的。
- 至于推导dp数组的重要性,动规专题里几乎每篇Carl都反复强调,当程序结果不对的时候,一定要自己推导公式,看看和程序打印的日志是否一样。