● 647. 回文子串 ● 516.最长回文子序列 ● 动态规划总结篇

本文详细解析了回文子串和最长回文子序列的动态规划解法,重点介绍了二维dp数组的定义、递推公式、初始化步骤以及遍历顺序。作者强调了动态规划中初始化和遍历顺序的重要性,指出它们对于正确解题至关重要。
摘要由CSDN通过智能技术生成

● 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];
    }
};

这两道题感觉体现了之前背包问题里面遍历顺序的重要性。

● 动态规划总结篇

动规基础:

背包问题:

打家劫舍问题:

股票问题:

子序列问题:

动规五部曲贯穿这几十道动规问题:

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

摘自代码随想录:

动规五部曲里,哪一部没想清楚,这道题目基本就做不出来,即使做出来了也没有想清楚,而是朦朦胧胧的就把题目过了。

  • (opens new window) 在这道题目中,初始化才是重头戏
  • 如果看过背包系列,特别是完全背包,那么两层for循环先后顺序绝对可以搞懵很多人,反而递归公式是简单的。
  • 至于推导dp数组的重要性,动规专题里几乎每篇Carl都反复强调,当程序结果不对的时候,一定要自己推导公式,看看和程序打印的日志是否一样。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值