代码随想录算法训练营Day-46 动态规划13 | 647. 回文子串、516.最长回文子序列、动态规划总结

647. 回文子串

动规五部曲

**dp数组含义:**dp[i][j]代表s[i,j]是否是回文子串,true或者false;

**递推公式:**当s[i]==s[j]相等,则需要看j和i差多少,要是差的值不超过2,则一定是回文子串;

如果差的值超过了2,则需要看dp[i+1][j-1],也就是区间左右缩减,看里面是不是回文子串,如果里面是,外面也一定是回文子串,如果里面不是外面也不是;

当s[i]和s[j]不相等,一定不是,直接false(初始化时已覆盖,该逻辑可以不写);

**初始化:**全部初始化为false即可,也就是默认全部都不是回文子串,通过逻辑一个个判别。

**遍历顺序:**由于递推公式方向是从右下到左上,所以是i是倒序遍历,j是顺序遍历,由于要组成区间,j>=i,所以j从i开始遍历;

cpp 复制代码
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] ==true){
                            dp[i][j] = true;
                            result++;
                        }
                    }
                }
            }
        }
        return result;
    }
};

516.最长回文子序列

动规五部曲

**dp数组含义:**dp[i][j]代表s[i,j]中的最长回文子序列长度;

**递推公式:**当s[i]==s[j]相等,则直接在dp[i+1][j-1]的基础上+2。因为对回文串两边加上一样的元素,还是回文子串,并且长度加了2;

当s[i]和s[j]不相等,左右可以尝试删除某一个,看删除哪个之后最长回文子序列长度最大,也就是max(dp[i+1][j],dp[i][j-1])

初始化: 由于递推是从左下到右上,左到右,下到上 三个方向递推,而且j是大于等于i 的,所以也就是右上半区域 ,所以最基础的情况就是对角线,所以要初始化对角线,也就是初始化单元素情况,全为1即可。

遍历顺序: 由于递推方向左下到右上,左到右,下到上可得出和上题相同,i倒序遍历,j顺序遍历

cpp 复制代码
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()-1; 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];
    }
};

动态规划总结

动态规划章节学习了背包问题、打家劫舍问题、股票问题、子序列问题(包括编辑距离、回文子串)

背包问题:抓住"物品能用几次"和"求什么"

背包问题先判断物品能不能重复使用。物品只能用一次,是 01 背包;物品可以重复使用,是完全背包。二维写法更直观,一维写法更常用,但一维写法必须特别注意遍历顺序。

01 背包一维递推通常是:

cpp 复制代码
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

因为每个物品只能用一次,所以背包容量 j 必须倒序遍历,避免同一个物品在本轮被重复使用。像 416、1049 本质都是 01 背包,只是value和weight可能都等于物品重量,目标是尽量装满某个容量。

完全背包的递推形式类似,但因为物品可以重复使用,所以容量通常正序遍历。若题目问组合数,例如 518 零钱兑换 II,要先遍历物品、再遍历背包,这样不会把不同顺序重复计算;若题目问排列数,例如 377 组合总和 IV,要先遍历背包、再遍历物品,这样不同顺序会被当成不同方案。

如果题目问最少个数,例如 322 零钱兑换、279 完全平方数,常见递推是:

cpp 复制代码
dp[j] = min(dp[j], dp[j - coin] + 1);

这类题重点不是方案数,而是"最小代价",所以初始化要特别注意:dp[0]=0,其他位置通常初始化为较大值,表示暂时不可达。

打家劫舍:本质是"当前取不取"

打家劫舍系列的核心判断是:当前位置偷,还是不偷。

198 是线性数组,递推很直接:

cpp 复制代码
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);

意思是第i家不偷,则继承i-1的结果;第 i 家偷,则必须加上i-2的结果。

213 是环形数组,因为首尾不能同时偷,所以拆成两个线性问题:不考虑最后一家,或者不考虑第一家,分别跑一次 198 的逻辑,最后取最大值。

337 是树形打家劫舍,不能再按数组顺序递推,而是用后序遍历。每个节点维护两个状态:偷当前节点、不偷当前节点。偷当前节点时,左右孩子不能偷;不偷当前节点时,左右孩子可以偷也可以不偷,取各自最大值。

股票问题:核心是"第几次交易"和"是否持有"

股票系列不要死记代码,先定义状态。最常见的状态是:第i天,当前是否持有股票,以及已经完成了几次交易。

121 只能买卖一次,维护最低买入价格即可,也可以写成两个状态:持有、不持有。

122 可以买卖无限次,只要上涨就可以累加利润。用动态规划时也是两个状态:持有股票和不持有股票。

123 最多买卖两次,可以扩展成五个状态:无操作、第一次持有、第一次不持有、第二次持有、第二次不持有。

188 最多买卖 k 次,本质是把 123 的状态推广到2k+1个状态。奇数状态通常表示持有,偶数状态表示卖出后不持有。

309 加入冷冻期后,不持有状态要进一步拆开,例如:保持不持有、今天卖出、冷冻期。区别在于今天能不能买入。

714 加入手续费后,本质仍然是持有和不持有,只是在买入或卖出时扣手续费即可。

子序列问题:区分"连续"和"不连续"

子序列问题最容易混的是:连续子数组、普通子序列、编辑距离、回文子序列。判断标准是:题目是否要求连续。

最长连续公共子数组,例如 718,要求连续,所以如果两个元素不相等,当前连续长度直接断掉,不从左边或上边取最大值。递推通常是:

cpp 复制代码
if (nums1[i - 1] == nums2[j - 1])
    dp[i][j] = dp[i - 1][j - 1] + 1;

最长公共子序列,例如 1143,不要求连续,所以不相等时要继承前面的最优结果:

cpp 复制代码
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

判断子序列,例如 392,本质是看短字符串能不能被长字符串匹配出来。因为只能在长字符串中跳过字符,所以不匹配时通常继承 dp[i][j - 1],而不是像 LCS 那样两边都取最大。

编辑距离类题目,例如 583、72,要围绕"增、删、改"来理解。583 只允许删除,所以本质是让两个字符串变成相同;72 可以增删改,所以状态转移更完整。

回文类题目要注意遍历方向。516 最长回文子序列中,dp[i][j] 表示区间 [i, j] 内的最长回文子序列长度。因为它依赖 dp[i + 1][j - 1]、dp[i + 1][j]、dp[i][j - 1],所以 i 要从后往前遍历,j 从 i + 1 往后遍历。对角线 dp[i][i] = 1,因为单个字符本身就是长度为 1 的回文子序列。

总结:动态规划的主线

动态规划不是背公式,而是先定义状态,再让状态之间发生转移。

背包问题看"物品使用次数"和"组合/排列/最值";打家劫舍看"偷不偷";股票问题看"持有状态和交易次数";子序列问题看"是否连续、是否允许编辑、区间如何缩小"。

只要dp含义、递推公式、初始化、遍历顺序能互相解释,代码就能搞定。


相关推荐
学习3人组1 小时前
柔性排产时序算法+中间过程+阶段目标 细化表格
算法·mes
挨踢ren1 小时前
单例模式:C++实现与多线程安全
c++·设计模式
he___H1 小时前
算法快与慢--哈希+双指针
算法·leetcode·哈希算法
呃呃本1 小时前
算法题(回溯)
算法
用户805533698031 小时前
现代Qt开发教程(新手篇)1.14——日志
c++·qt
刀法如飞2 小时前
Rust数组去重的20种实现方式,AI时代用不同思路解决问题
人工智能·算法·ai编程
yxc_inspire2 小时前
25年CCPC福建邀请赛补题
学习·算法
Raink老师2 小时前
用100道题拿下你的算法面试(链表篇-4):合并 K 个有序链表
算法·链表·面试
Liangwei Lin2 小时前
LeetCode 20. 有效的括号
算法