数据结构与算法-动态规划(基础框架+子序列问题)

 问题汇总:

1.如何选择使用递归法解题还是迭代法解题

(我猜是做的多了背的题多了就自然懂了)

2.迭代法有没有可以去重的空间和套路

迭代法一般没有通用去重方式,因为已经相当于递归去重后了

这两个问题其实是一个问题,一般直接写出的没有去重的递归法,复杂度很高,此时需要使用备忘录去重,而备忘录去重时间复杂度和使用dp数组进行迭代求解时间复杂度相同,但是由于递归需要反复调用函数,实际开销更加多

综上,一般使用dp数组法最优

一、动态规划基本框架

1.1 动态规划用于解决的问题?(如何判断能否使用dp)

动态规划 = 最优子结构 + 重叠子问题 + 转移方程

最优子结构保证能从局部解推出全局解,也就是保证能够写出转移方程

重叠子问题说明暴力解法太耗时,我们可以使用动态规划进行优化

(期待后期的更改,相信未来的我的智慧!)

「最优子结构」是某些问题的一种特定性质,并不是动态规划问题专有的。也就是说,很多问题其实都具有最优子结构,只是其中大部分不具有重叠子问题,所以我们不把它们归为动态规划系列问题而已。

我先举个很容易理解的例子:假设你们学校有 10 个班,你已经计算出了每个班的最高考试成绩。那么现在我要求你计算全校最高的成绩,你会不会算?当然会,而且你不用重新遍历全校学生的分数进行比较,而是只要在这 10 个最高成绩中取最大的就是全校的最高成绩。

我给你提出的这个问题就符合最优子结构:可以从子问题的最优结果推出更大规模问题的最优结果。让你算每个班的最优成绩就是子问题,你知道所有子问题的答案后,就可以借此推出全校学生的最优成绩这个规模更大的问题的答案。

你看,这么简单的问题都有最优子结构性质,只是因为显然没有重叠子问题,所以我们简单地求最值肯定用不出动态规划。

再举个例子:假设你们学校有 10 个班,你已知每个班的最大分数差(最高分和最低分的差值)。那么现在我让你计算全校学生中的最大分数差,你会不会算?可以想办法算,但是肯定不能通过已知的这 10 个班的最大分数差推到出来。因为这 10 个班的最大分数差不一定就包含全校学生的最大分数差,比如全校的最大分数差可能是 3 班的最高分和 6 班的最低分之差。

这次我给你提出的问题就不符合最优子结构,因为你没办通过每个班的最优值推出全校的最优值,没办法通过子问题的最优值推出规模更大的问题的最优值。前文 动态规划详解 说过,想满足最优子结,子问题之间必须互相独立。全校的最大分数差可能出现在两个班之间,显然子问题不独立,所以这个问题本身不符合最优子结构。

那么遇到这种最优子结构失效情况,怎么办?策略是:改造问题。对于最大分数差这个问题,我们不是没办法利用已知的每个班的分数差吗,那我只能这样写一段暴力代码:

int result = 0;
for (Student a : school) {
    for (Student b : school) {
        if (a is b) continue;
        result = max(result, |a.score - b.score|);
    }
}
return result;

改造问题,也就是把问题等价转化:最大分数差,不就等价于最高分数和最低分数的差么,那不就是要求最高和最低分数么,不就是我们讨论的第一个问题么,不就具有最优子结构了么?那现在改变思路,借助最优子结构解决最值问题,再回过头解决最大分数差问题,是不是就高效多了?

1.2 迭代(自底向上)-五部曲(自底向上的迭代求解)

1.2.1 明确dp数组下标和下标对应的值的含义

这一步其实是要和转移方程结合理解的,很多时候我们是为了能够写出转移方程,才进行某种dp数组的定义

所以这一步非常重要,我们需要知道问题中有哪些状态才能为dp数组合理分配这些状态

所谓状态,也就是原问题和子问题中会变化的变量。我们要将这些变化的量分配给dp数组的下标和值

注意:比较难的题,状态不能一眼看出来,需要我们自己进行构造,这样的题只有靠积累

简单例子

数据结构与算法-动态规划(基础框架+子序列问题)_第1张图片
1:斐波那契的dp[]定义:i 表示n = i,dp[i]表示n = i对应的结果是什么,这样的逻辑非常清晰

 数据结构与算法-动态规划(基础框架+子序列问题)_第2张图片

2:凑零钱:(注意虽然硬币个数不会变化,因为硬币数量无限,所以有两个变化量,凑零钱的硬币数和目标金额),所以我们定义i为目标金额,dp[i]值为零钱个数(一般都会吧dp[i]当作输出结果)

这里会有一点歧义 ,我们要知道我们最后要得到的是什么,应该是amount对应的最小数量,那么dp[i]应该设置为金额i对应的最小数量,不要思考成我们将dp[i]定义为i对应的所有数量,然后通过比较所有dp[i]来求得最小值,这样似乎更容易理解,但是却不实际,那样dp[i]会被定义为二维数组,复杂度也上升了

我们应该知道,最小这个限制,是在转移方程里面实现的,所以我们默认得到的dp[i]就是最小值!

困难例子

最长递增子序列:我们发现问题中貌似没有直接给出变化的状态,从而无从下手

这是子序列问题中的一类问题,处理方法有相对固定几类,我们这里就直接套用经典定义

dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度

对于其他不明确状态的问题如何定义,这个也要靠积累,需要我们多做题才知道

1.2.2 确定递推公式(转移方程)

动态规划的核心设计思想是数学归纳法。

相信大家对数学归纳法都不陌生,高中就学过,而且思路很简单。比如我们想证明一个数学结论,那么我们先假设这个结论在 k < n 时成立,然后根据这个假设,想办法推导证明出 k = n 的时候此结论也成立。如果能够证明出来,那么就说明这个结论对于 k 等于任何数都成立。

类似的,我们设计动态规划算法,不是需要一个 dp 数组吗?我们可以假设 dp[0...i-1] 都已经被算出来了,然后问自己:怎么通过这些结果算出 dp[i]

直接拿最长递增子序列这个问题举例你就明白了。不过,首先要定义清楚 dp 数组的含义,即 dp[i] 的值到底代表着什么?

我们的定义是这样的:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度(子序列中要包含nums[i],可能我们会想这样不能找到实际的最大子序列,但是其实我们遍历了所有nums[i],并不会漏掉)

数据结构与算法-动态规划(基础框架+子序列问题)_第3张图片

假设我们已经知道了 dp[0..4] 的所有结果,我们如何通过这些已知结果推出 dp[5] 

这就是求解转移方程的重点一步,此处

根据刚才我们对 dp 数组的定义,现在想求 dp[5] 的值,也就是想求以 nums[5] 为结尾的最长递增子序列。

其实就是自己总结规律,然后使用数学归纳法验证

光靠脑袋想dp的递推公式确实很困难 我的做法是画一个dp数组出来 找一个例子填进去,然后通过观察找出规律倒推递推公式(仅供参考,有待验证)

当我们发现递推公式不好找时,需要思考是否改变dp定义

nums[5] = 3,既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到这些子序列末尾,就可以形成一个新的递增子序列,而且这个新的子序列长度加一

同时我们是在找最长的子序列,所以我们需要找出前面序列中的最长序列接上去

我们的转移方程为:

dp[i] = dp[j] + 1;//j是比i小的数

 接下来就是要将取最大值还有求比i小的j结合起来

代码如下:

    for(int j = 0;j < i;j++){        
        if (nums[i] > nums[j]) {
            // 把 nums[i] 接在后面,即可形成长度为 dp[j] + 1,
            // 且以 nums[i] 为结尾的递增子序列
            dp[i] = Math.max(dp[i], dp[j] + 1);
        }
    }

这整个代码才是完整的转移方程 

一个重点:

我们一定要假设已经知道了dp[i-1],并且我们设置的dp数组可以存放的值就是我们希望的值

1.2.3 dp数组初始化和base case

初始化要结合我们对dp数组的定义进行设置,并且结合保证在执行max和min操作时不会影响更新值,对dp数组的定义不同的话,初始化也会有差别

(有其他理解后面补充)

base case 就是递归中的最底层,是所有子问题中的最基础的问题,靠这个问题推出其他所有解

初始化非常重要,它和转移方程共同决定了我们定义的dp[i]里面的值是否正确,一般是确定了dp定义和转移方程,再开始初始化

1.2.4 确定遍历顺序

现在接触到的都是顺序进行遍历的,并没有在遍历时挖坑,但是后面有从前往后,从后往前还有斜着遍历

这些情况的分析等到做到具体的题目再来补充

将转移方程放入遍历中:

        for(int i = 0;i < nums.size();i++)
            for(int j = 0;j < i;j++){
                if(nums[i] > nums[j]){
                    dp[i] = max(dp[i],dp[j]+1);
                }
            }

1.2.5 dp数组打印

通过递推公式手写出dp数组,用于检测dp哪里写错了

1.3 递归(自顶向上)

1.3.1 暴力解法

按道理来说,应该是先写出递归法,然后通过备忘录进行优化,再然后使用dp数组的解法来解答,这里由于dp数组法有一套成熟的流程,我这里想写递归法时,会将迭代法先写出来,然后使用套用到递归中。

1.3.2 备忘录去重

明确了问题,其实就已经把问题解决了一半。即然耗时的原因是重复计算,那么我们可以造一个「备忘录」,每次算出某个子问题的答案后别急着返回,先记到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。

注意:递归法使用备忘录去重后,尽管时间复杂度和迭代相同,但是,我们递归会不断的在栈上调用函数,造成大量的内存和时间消耗,这会导致实际运行时,递归法的时间空间消耗大于迭代法

详情参考:

第七章 C语言函数_递归函数的致命缺陷:巨大的时间开销和内存开销(附带优化方案)_c++ 递归资源消耗-CSDN博客

1.3.3 dp table去重

就是迭代法,这样的做法时间复杂度和备忘录去重一致

二、经典dp问题-用于理解dp的做法和细节

2.1 零钱兑换

题目链接:322. 零钱兑换 - 力扣(LeetCode)

 数据结构与算法-动态规划(基础框架+子序列问题)_第4张图片

(1)dp定义:

首先确定状态,问题中改变的状态只有总金额和使用的硬币个数(硬币总数无限,所以剩下的硬币数量不变化)

我们将dp[i]和i与变化量进行匹配,从而有如下设置:

i为总金额。dp[i]为达到总金额i需要的硬币数量

(2)转移方程

我们这样想,肯定是要建立dp[i]和dp[j]的联系,但实际这个i j应该如何取,也就是总金额如何变化更合理,所以自然想到了,每次对总金额 i 减少一个硬币的值,来求得 j ,那么关系如何建立呢

这就要用到上面的数学归纳法了,假设我们已经知道减少一个硬币后的dp[i - coin]的值,那么dp[i]的最小值就是dp[i - coin]+1

(3)初始化

由于我们的i要索引到amount,索引dp的size要为amount+1,同时对于dp[i]来说,最差的情况即使由i个一元硬币组成,那么最大个数为i,为了使得初始状态不影响结果,需要将dp[i] 赋值 i+1,为了省事直接赋值最大的amount+1,代码如下:

vector dp(amount+1,amount+1);

对于base case,我们知道dp[0] = 0,但是注意dp[1] != 1,因为有一个硬币但不一定是一个一元的硬币,所有可能dp[1] = -1,因此不能将dp[1]放入base case中,代码如下:

        dp[0] = 0;
        // dp[1] = 1;

(4)确定遍历方向:

此题为顺序遍历,但是也要仔细理解一下

非常类似bfs的理解过程,我们先对dp[i]中所有金额从0-amount进行遍历,保证能够处理到所有的i(类似于走通每一层),然后对于每一个i我们都将所有的coin去除一次(所有的选择都选一次),选出最小的情况然后赋值给dp[i]

代码如下:

        for(int i = 0;i < amount+1;i++)
            for(int coin : coins){
                if(i - coin < 0)continue;
                dp[i] = min(dp[i],dp[i - coin] + 1);
            }

5 :打印dp数组进行debug,此题过程比较简单就不演示了

完整代码如下:

class Solution {
public:
    int coinChange(vector& coins, int amount) {
        vector dp(amount+1,amount+1);
        if(amount == 0)return 0;
        dp[0] = 0;
        // dp[1] = 1;
        for(int i = 0;i < amount+1;i++)
            for(int coin : coins){
                if(i - coin < 0)continue;
                dp[i] = min(dp[i],dp[i - coin] + 1);
            }
        return dp[amount] == amount+1 ? -1 : dp[amount];
    }
};

2.2 斐波那契数列

题目链接:509. 斐波那契数 - 力扣(LeetCode)

数据结构与算法-动态规划(基础框架+子序列问题)_第5张图片

此题已经给出转移方程,并且dp数组的定义也较为清楚,base case给出,初始化时没有特殊要求,我们直接初始化为0就行了,并且也是顺序遍历,所有很容易写出如下代码:

class Solution {
public:
    int fib(int n) {
        if(n==0) return 0;
        vector dp(n+1,0);
        dp[0] = 0;
        dp[1] = 1;
        for(int i = 2;i <= n;i++)
            dp[i] = dp[i-1] +dp[i-2];
        return dp[n];
    }
};

由于只涉及到两个数的处理,所以我们可以只保留这两个数,有如下优化代码:

class Solution {
public:
    int fib(int n) {
        if(n==0)return 0;
        if(n==1)return 1;
        int p=0,q=1,res=0;
        for(int i = 2;i <= n;i++){
            res = p + q;
            p = q;
            q = res;
        }
        return res;
    }
};

 三、子序列问题

子序列子串问题总结:

1.dp定义

一般有两种dp定义

1)dp[i]表示以nums[i]结尾(包含i)的结果

2)dp[i]表示以[0,i]的子串(不要求包含i)的结果

单序列问题,一般使用(1),因为只有一条单序列,(1)更容易将前后关系链接起来

双序列问题:
一般要求连续(子串,子数组)就使用(1),不连续(子序列)就使用(2)

(注意:为例初始化方便,一般的dp[i][j]对应nums[i-1]和nums2[j-1])

2.转移方程

转移方程的确立本质上就是分情况讨论

对于单序列,我们一般会判断nums[i]和nums[i-1]的关系,或者判断dp[i-1]等等,分情况讨论如何从dp[i-1]转移到dp[i]

(单序列的判断更加灵活)

对于双序列问题,一般会对比nums1[i]和nums2[j],通常相同时,我们考虑dp[i-1][j-1]转移到dp[i][j],而不同时考虑dp[i-1][j-1],dp[i][j-1],dp[i-1][j]如何转移到dp[i][j],例如下图:

数据结构与算法-动态规划(基础框架+子序列问题)_第6张图片

如何转移?
一般都是通过+的形式进行转移

3.初始化

这一步需要在转移方程之后写,因为我们要确定转移方程可行,才能开始初始化,首先我们要画出如下的dp数组(此处针对双序列,单序列一般只初始化dp[0])

数据结构与算法-动态规划(基础框架+子序列问题)_第7张图片

 根据dp数组定义在橙色位置填上初始值,然后可以使用转移方程检查一下合理

图中红字很重要

4.遍历顺序

从初始化开始往后遍历即可,类似下图:

数据结构与算法-动态规划(基础框架+子序列问题)_第8张图片

5.检查dp数组

觉得有问题,就把dp数组打印出来, 看和画的一样不

3.1单序列问题

3.1.1 最长递增子序列

题目链接:300. 最长递增子序列 - 力扣(LeetCode)

数据结构与算法-动态规划(基础框架+子序列问题)_第9张图片

1:首先确定如何定义dp数组

我们使用子序列一般定义方式,定义dp[i]为以nums[i]结尾的子序列的解(也即是这一部分的严格递增子序列最大长度) ,注意:这里特别还有一点,求得的最大子序列必须包含nums[i],为了和之后的转移方程进行匹配

2:确定转移方程

转移方程的确定一定要根据数学归纳法来求解,因为我们要求dp[nums.size()],所以我们假设知道dp[0,.....,nums.size()-1],直接理解不是很清晰,作图如下:
数据结构与算法-动态规划(基础框架+子序列问题)_第10张图片

为了推导到更一般的情况,我们假设前面已知的为dp[i],当前需要求得的时dp[j],因为每一个子序列的解必须包含nums[i],所以如果当前的nums[j]比前面某一个nums[i]大的话,就可以直接接在后面,那么dp[j]的解至少也是dp[i]+1。

最后想要求得dp[j]实际上比dp[i]大多少,我们只需要比较所有比nums[j]小的nums[i],选取最大的dp[i]进行+1即可

因此转移方程为:

for(int i = 0;i < j; i++)
    if(nums[j] > nums[i])
        dp[j] = max(dp[j],dp[i]+1)

3:确定初始状态

由于dp[i]表示nums[i]结尾且包含nums[i]的解,所有解的长度至少为1,也就是nums[i]本身,因此我们需要对dp[i]初始化为1;

4:遍历顺序

由于我们求解dp[j]时会用到dp[0,..,j-1]所有我们顺序遍历即可

5:打印dp

数据结构与算法-动态规划(基础框架+子序列问题)_第11张图片

完整代码如下:

dp数组法
class Solution {
public:
    int lengthOfLIS(vector& nums) {
        int n = nums.size();
        vector dp(n,1);
        dp[0] = 1;
        for(int i = 0;i < nums.size();i++)
            for(int j = 0;j < i;j++){
                if(nums[i] > nums[j]){
                    dp[i] = max(dp[i],dp[j]+1);
                }
            }
        int res=0;
        for(int i : dp){
            res = max(res,i);
        }
        return  res;
    }
};

时间复杂度O(n^2)

此题有一种更加巧妙的解法,可以进一步降低时间复杂度:

二分法:
    int lengthOfLIS(vector& nums) {
        int piles = 0;    // 牌堆数初始化为 0
        vector top(nums.size());   // 牌堆数组 top
        
        for (int i = 0; i < nums.size(); i++) {
            int poker = nums[i];    // 要处理的扑克牌

            /***** 搜索左侧边界的二分查找 *****/
            int left = 0, right = piles;    // 搜索区间为 [left, right)
            while (left < right) {
                int mid = (left + right) / 2;    // 防溢出
                if (top[mid] > poker) {
                    right = mid;
                } else if (top[mid] < poker) {
                    left = mid + 1;
                } else {
                    right = mid;
                }
            }
            /*********************************/

            // 没找到合适的牌堆,新建一堆
            if (left == piles) piles++;
            // 把这张牌放到牌堆顶
            top[left] = poker;
        }
        // 牌堆数就是 LIS 长度
        return piles;
    }

时间复杂度O(n*log2n) 

 3.1.2  俄罗斯套娃信封

数据结构与算法-动态规划(基础框架+子序列问题)_第12张图片

 此题为最长递增递增子序列的二维形式,本质还是找形状递增的最长子信封,主要问题在于,我们如何将形状引入进行比较

此处处理十分的巧妙:

先对宽度 w 进行升序排序,如果遇到 w 相同的情况,则按照高度 h 降序排序;之后把所有的 h 作为一个数组,在这个数组上计算 LIS 的长度就是答案

数据结构与算法-动态规划(基础框架+子序列问题)_第13张图片

为什么是这样呢,因为我们先使得宽度排好序,然后对高度求最长递增子序列,就能得到递增的信封,但是考虑到相同宽度不能相互装,也即是同一宽度我们只能使用一个信封,所以我们选择将同一宽度下的高度进行降序,这样在降序中,只有可能取其中之一(因为我们找的是升序排列) 

 lambda

这里引入lambda表达式进行排序:

c++ lambda 看这篇就够了!(有点详细)_c++ 运行时 构建 lamda-CSDN博客

        sort(envelopes.begin(), envelopes.end(), 
            [](const vector a, const vector& b) {
                // return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0];
                if (a[0] != b[0]) {
                    return a[0] < b[0]; // 按宽度升序排序
                } else {
                    return a[1] > b[1]; // 如果宽度相同,按高度升序排序
                }
                });

然后我们对排好序的信封的高进行最长递增子序列求解,直接调用函数即可

dp数组法:

    int maxseq(vector& seq) {
        int size = seq.size(),res= 0;
        vector dp(size,1);
        dp[0] = 1;
        for(int i = 0;i< size;i++){
            for(int j = 0;j< i;j++){
                if(seq[i] > seq[j]){
                    dp[i] = max(dp[i],dp[j]+1);
                }
            }
            res = max(dp[i],res);   
        }
        return res;
    }

二分法:

    int lengthOfLIS(vector& nums) {
        int piles = 0;    // 牌堆数初始化为 0
        vector top(nums.size());   // 牌堆数组 top
        
        for (int i = 0; i < nums.size(); i++) {
            int poker = nums[i];    // 要处理的扑克牌

            /***** 搜索左侧边界的二分查找 *****/
            int left = 0, right = piles;    // 搜索区间为 [left, right)
            while (left < right) {
                int mid = (left + right) / 2;    // 防溢出
                if (top[mid] > poker) {
                    right = mid;
                } else if (top[mid] < poker) {
                    left = mid + 1;
                } else {
                    right = mid;
                }
            }
            /*********************************/

            // 没找到合适的牌堆,新建一堆
            if (left == piles) piles++;
            // 把这张牌放到牌堆顶
            top[left] = poker;
        }
        // 牌堆数就是 LIS 长度
        return piles;
    }

值得一提的是,本题由于一些恶心的例子,只有使用二分法求解递增子序列才不会超时

完整代码如下:

class Solution {
public:
//超时
    int maxseq(vector& seq) {
        int size = seq.size(),res= 0;
        vector dp(size,1);
        dp[0] = 1;
        for(int i = 0;i< size;i++){
            for(int j = 0;j< i;j++){
                if(seq[i] > seq[j]){
                    dp[i] = max(dp[i],dp[j]+1);
                }
            }
            res = max(dp[i],res);   
        }
        return res;
    }
//不超时
    int lengthOfLIS(vector& nums) {
        int piles = 0;    // 牌堆数初始化为 0
        vector top(nums.size());   // 牌堆数组 top
        
        for (int i = 0; i < nums.size(); i++) {
            int poker = nums[i];    // 要处理的扑克牌

            /***** 搜索左侧边界的二分查找 *****/
            int left = 0, right = piles;    // 搜索区间为 [left, right)
            while (left < right) {
                int mid = (left + right) / 2;    // 防溢出
                if (top[mid] > poker) {
                    right = mid;
                } else if (top[mid] < poker) {
                    left = mid + 1;
                } else {
                    right = mid;
                }
            }
            /*********************************/

            // 没找到合适的牌堆,新建一堆
            if (left == piles) piles++;
            // 把这张牌放到牌堆顶
            top[left] = poker;
        }
        // 牌堆数就是 LIS 长度
        return piles;
    }

    int maxEnvelopes(vector>& envelopes) {
        int n = envelopes.size();
        // 按宽度升序排列,如果宽度一样,则按高度降序排列
        sort(envelopes.begin(), envelopes.end(), 
            [](const vector a, const vector& b) {
                // return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0];
                if (a[0] != b[0]) {
                    return a[0] < b[0]; // 按宽度升序排序
                } else {
                    return a[1] > b[1]; // 如果宽度相同,按高度升序排序
                }
                });

        // 对高度数组寻找 LIS
        vector height(n);
        for (int i = 0; i < n; i++){
            height[i] = envelopes[i][1];
            // cout<

3.1.3 最长连续递增子序列 

题目链接:674. 最长连续递增序列 - 力扣(LeetCode)

数据结构与算法-动态规划(基础框架+子序列问题)_第14张图片

因为要求连续,所以我们对在上一题基础上,只对相邻两个值进行判断, 转移方程如下:

            if(nums[i] > nums[i-1])
                dp[i] = dp[i-1]+1;
            res = max(res,dp[i]);

完整代码如下:

class Solution {
public:
    int findLengthOfLCIS(vector& nums) {
        if(nums.size()==0)return 0;
        int res =1;
        vector dp(nums.size(),1);
        for(int i = 1;i < nums.size() ;i++){
            if(nums[i] > nums[i-1])
                dp[i] = dp[i-1]+1;
            res = max(res,dp[i]);//上一题的优化部分,边求解边求最大值,省时间
        }
        
        return res;
    }
};

3.1.4 最大子序列和--转移方程涉及对以nums[i]结尾的理解

数据结构与算法-动态规划(基础框架+子序列问题)_第15张图片

 dp定义:

dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]。

 转移方程:

依据题意,我们会很自然的想到,要通过nums[i]是否让子序列继续保持增长来作为判断条件,但事实上这样无法判断,因为当nums[i]+dp[i-1] < dp[i-1]时,也可能保留原序列继续扩展,如图:数据结构与算法-动态规划(基础框架+子序列问题)_第16张图片

因此我们重新思考判断条件,首先我们要明确,得到的子序列要以nums[i]结尾,所以必须以nums[i]为基准进行参考,也就是先思考一定要留下nums[i],再思考是否留下前面的子序列,应该是考虑dp[i-1]是否大于0,dp[i-1]大于0的话,nums[i]为结尾的子序列可以增加,那么可以将nums[i]接到前面子序列的后面,如果dp[i-1]小于0,那么 ums[i]为结尾的子序列加上dp[i-1]就会减少,所以我们直接保留nums[i]即可,因此代码如下图:

            if(dp[i-1] < 0)
            dp[i] = nums[i];
            else
            dp[i] = dp[i-1] + nums[i];

简化一下:

            dp[i] = max(dp[i-1]+nums[i],nums[i]);

初始化

因为dp[i]由dp[i-1]决定,那么dp[0]就是最开始的初始状态.
 根据dp[i]的定义,很明显dp[0]应为nums[0]即dp[0] = nums[0]

确定遍历顺序

递推公式中dp[i]依赖于dp[i - 1]的状态,需要从前向后遍历。

完整代码如下:

class Solution {
public:
    int maxSubArray(vector& nums) {
        int n = nums.size(),res = INT_MIN;
        vector dp(nums);
        res = dp[0];// 因为循环是从1开始,遍历不到0,所以此处直接将nums【0】赋给res,模拟一个第一轮
        for(int i = 1;i < n;i++){
            // if(dp[i-1] < 0)
            // dp[i] = nums[i];
            // else
            // dp[i] = dp[i-1] + nums[i];
            dp[i] = max(dp[i-1]+nums[i],nums[i]);
            res= max(dp[i],res);
        }
        return res;
    }   
};

注意此处,res不是从0开始进行遍历的,那么会漏掉对dp[0]的比较,所以我们需要将dp[0]初始化为res的初值(为了不再后面再进行一层循环来求解res的最大值)

由于这里dp[i]只和dp[i-1]有关,所以我们可以使用状态压缩,使用两个变量保持dp[i]和dp[i-1]而不使用dp数组,这样空间复杂度会进一步降低

//状态压缩
class Solution {
public:
    int maxSubArray(vector& nums) {
        int n = nums.size(),res = INT_MIN;
        int dp_0=nums[0],dp_1;
        res = nums[0];
        for(int i = 1;i < n;i++){
            dp_1 = max(dp_0+nums[i],nums[i]);
            dp_0 = dp_1;
            res= max(dp_1,res);
        }
        return res;
    }   
};

此题还可以使用双指针进行解答,代码如下,具体解释减数组双指针一节:

// 双指针法
class Solution {
public:
    int maxSubArray(vector& nums) {
        // if(nums.size()==1)return nums[0];
        int l=0,r=0;
        int sum=0,maxsum=INT_MIN;
        cout<

3.2 双序列问题

3.2.1 编辑距离--很重要的母体

 数据结构与算法-动态规划(基础框架+子序列问题)_第17张图片

 迭代法:

 定义:

和一般的子序列问题一样,我们对dp数组的定义为:

dp[i][j]  为以word1[i-1]和word2[j-1]结尾的两个字符串的编辑距离 

为了方便初始化,后面会细说 

 转移方程:
这里的转移方程是最复杂的,我们要分别求解不同操作对dp[i][j]的影响。

注意:我们要从后往前操作,因为在前面插入会导致前面字符的索引更改!!!

比较word1[i]和word2[j] 

插入和删除:

dp[i][j] = dp[i-1][j]+1//在word1进行插入或者删除

dp[i][j] = dp[i][j-1]+1//在word2进行插入或者删除

替换:

dp[i][j] = dp[i-1][j-1]+1//在word1或者word2进行替换

相等:

dp[i][j] = dp[i-1][j-1];

因此,转移方程为:

           if(s1[i-1] == s2[j-1])dp[i][j] = dp[i-1][j-1];
           else{
                dp[i][j] = min(
                        dp[i][j-1]+1,
                        dp[i-1][j]+1,
                        dp[i-1][j-1]+1
                );
           }

初始化 :
我们要知道,什么时候能一眼看出答案——当其中一个字符串为空时,这里就涉及到dp数组的定义了,因为如果dp[i][j]为word1[i]和word2[j]结尾的子串的编辑距离,那么对于长度为0的字符串(空串)就无法定义,因此,我们设置dp[i][j] 为word1[i-1]和word2[j-1]结尾的子串的编辑距离

同时,注意dp 数组的每个维度比相应的字符串长度多一个单位,以便包含空字符串的情况。这就是为什么数组大小是 m+1 x n+1。因此初始化如下:

        vector> dp(m+1,vector(n+1,0));
        // for(int i = 0;i < m;i++)
        //     dp[i][0] = i;
        // for(int j = 0;j < n;j++)
        //     dp[0][j] = j;
        for(int i = 0;i < m+1;i++)
            dp[i][0] = i;
        for(int j = 0;j < n+1;j++)
            dp[0][j] = j;

 遍历顺序:

注意,这里我们虽然是从后往前处理,也就是说我们要先知道前面的才能处理后面的,因此遍历的时候需要从前往后遍历

另一方面可以这样思考:

如图
数据结构与算法-动态规划(基础框架+子序列问题)_第18张图片

可以看出dp[i][j]是依赖左方,上方和左上方元素的,所以在dp矩阵中一定是从左到右从上到下去遍历。 

将转移方程放入遍历中:

        for(int i = 1;i <=m ;i++)
            for(int j = 1; j <= n;j++) 
            {
                if(s1[i-1] == s2[j-1])dp[i][j] = dp[i-1][j-1];
                else{
                    dp[i][j] = min(
                        dp[i][j-1]+1,
                        dp[i-1][j]+1,
                        dp[i-1][j-1]+1
                    );
                }
            }

最后,添加特殊处理的部分,代码如下:

class Solution {
public:
    int min(int a, int b, int c) {
        return std::min(std::min(a, b), c);
    }
    int minDistance(string s1, string s2) {
        int m = s1.size(),n = s2.size();
        if(m==0)return n;
        if(n==0)return m;
        //int* dp = new int[m][n];二维数组不好使用new的方式进行实现

        //int[][] dp = new int[m + 1][n + 1];这样是对的
        vector> dp(m+1,vector(n+1,0));
        for(int i = 0;i < m;i++)
            dp[i][0] = i;
        for(int j = 0;j < n;j++)
            dp[0][j] = j;
        for(int i = 1;i <=m ;i++)
            for(int j = 1; j <= n;j++) 
            {
                if(s1[i-1] == s2[j-1])dp[i][j] = dp[i-1][j-1];
                else{
                    dp[i][j] = min(
                        dp[i][j-1]+1,
                        dp[i-1][j]+1,
                        dp[i-1][j-1]+1
                    );
                }
            }
        return dp[m][n];
    }

};
递归法:

先明确递归的思路:
我们如何从迭代转换到递归

首先,状态转移,初始化和dp的定义是不变的,那么我们首先思考如何将dp[i][j]中的i,j体现在dp递归函数中,显而易见,答案是,传入两个参数,i,j;

    int dp(string s1,int i,string s2, int j)

接着是初始化,这里本来可以设置 dp[i][j]  为以word1[i]和word2[j]结尾的两个字符串的编辑距离,但为了和迭代法统一,就延续上述设置

        if(i==0)return j;
        if(j==0)return i;

转移方程,与迭代法十分类似:

        if(s1[i-1] == s2[j-1])return dp(s1,i-1,s2,j-1);
        else return min(
            dp(s1,i-1,s2,j)+1,
            dp(s1,i,s2,j-1)+1,
            dp(s1,i-1,s2,j-1)+1
        );

注意 ,递归法有遍历顺序,但是是自动执行的,递归中会自动执行,所以迭代法中的for循环部分可以省去:
完整代码如下:

class Solution {
public:

    int minDistance(string s1, string s2) {
        int m = s1.length(), n = s2.length();
        return dp(s1, m, s2, n);
    }

    int dp(string s1,int i,string s2, int j){
        if(i==0)return j;
        if(j==0)return i;
        if(s1[i-1] == s2[j-1])return dp(s1,i-1,s2,j-1);
        else return min(
            dp(s1,i-1,s2,j)+1,
            dp(s1,i,s2,j-1)+1,
            dp(s1,i-1,s2,j-1)+1
        );
    }

    int min(int a, int b, int c) {
        return std::min(std::min(a, b), c);
    }
};
递归去重:

备忘录:

我们增加一个备忘录,将结果保存到备忘录的对应位置

memo = vector>(m+1, vector(n+1, -1));

检测这个位置是否已经有值,有的话就不用再算了

        // 查备忘录,避免重叠子问题
        if (memo[i][j] != -1) {
            return memo[i][j];
        }
        // 状态转移,结果存入备忘录
        if (s1[i-1] == s2[j-1]) {
            memo[i][j] = dp(s1, i - 1, s2, j - 1);
        } else {
            memo[i][j] = min(
                dp(s1, i, s2, j - 1) + 1,
                dp(s1, i - 1, s2, j) + 1,
                dp(s1, i - 1, s2, j - 1) + 1
            );
        }
        return memo[i][j];

可能会疑惑,为什么能保证出现过一次的memo[i][j]就一定是最优解,后面不会更新比这个值更好的解吗?

数据结构与算法-动态规划(基础框架+子序列问题)_第19张图片

我们可以看到,每次求解dp[i][j],都是使用三种状态,那么第二次对dp[i][j]进行处理的话,也会进行同样的处理,结果是没变的

因此,完整代码如下:

class Solution {
public:
    vector> memo;
    int minDistance(string s1, string s2) {
        int m = s1.length(), n = s2.length();
        memo = vector>(m+1,vector(n+1,-1));
        return dp(s1, m, s2, n);
    }


    int dp(string s1,int i,string s2, int j){
        if(i==0)return j;
        if(j==0)return i;
        if(memo[i][j] != -1) return memo[i][j];
        if(s1[i-1] == s2[j-1])memo[i][j] =  dp(s1,i-1,s2,j-1);
        else memo[i][j] = min(
            dp(s1,i-1,s2,j)+1,
            dp(s1,i,s2,j-1)+1,
            dp(s1,i-1,s2,j-1)+1
        );

        return memo[i][j];
    }

    int min(int a, int b, int c) {
        return std::min(std::min(a, b), c);
    }
};

迭代法:

数据结构与算法-动态规划(基础框架+子序列问题)_第20张图片

递归+备忘录:

数据结构与算法-动态规划(基础框架+子序列问题)_第21张图片

可以看出,迭代法递归法复杂度相同时,由于递归不断调用函数产生资源消耗,其运行效率远不如迭代法

3.2.2 最长重复子数组

数据结构与算法-动态规划(基础框架+子序列问题)_第22张图片

dp数组(dp table)以及下标的含义

 dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 (特别注意: “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串 )

(此处定义同编辑距离,为了初始化方便)

转移方程:

做过编辑距离后发现,此题比较简单,因为我们dp[i][j]必须包含nums1[i-1]和nums2[j-1],所以只需要比较nums1[i-1]和nums[j-1]即可,当他们不等时,dp[i][j]=0

因此,转移方程如下:

                if(nums1[i-1] == nums2[j-1]){
                    dp[i][j] = dp[i-1][j-1] + 1;
                    res = max(res,dp[i][j]);
                }

 dp数组初始化

DP数组如下,我们发现我们需要对第一排和第一列进行初始化,才能顺利推导到后面的值

数据结构与算法-动态规划(基础框架+子序列问题)_第23张图片

根据dp数组的意义,我们将其初始化为0即可

同时,由于某些dp值,我们会赋值为0,所以我们直接将所以初值都设置为0,当遇到应该为0的dp值,我们不更新即可,代码如下:

        vector> dp(nums1.size()+1,vector(nums2.size()+1,0));

遍历顺序:
外层for循环遍历A,内层for循环遍历B。

外层for循环遍历B,内层for循环遍历A。都行

完整代码如下:

class Solution {
public:
    int findLength(vector& nums1, vector& nums2) {
        vector> dp(nums1.size()+1,vector(nums2.size()+1,0));
        int res = 0;
        for(int i = 1;i <= nums1.size();i++)//边界问题
            for(int j = 1;j <= nums2.size();j++){
                if(nums1[i-1] == nums2[j-1]){
                    dp[i][j] = dp[i-1][j-1] + 1;
                    res = max(res,dp[i][j]);
                }
            }
        return res;
    }
};

3.2.3  最长重复子序列

数据结构与算法-动态规划(基础框架+子序列问题)_第24张图片

 本题和最长重复子数组区别在于这里不要求是连续的了,所以dp数组的定义不用必须以nums[i]结尾了

确定dp数组(dp table)以及下标的含义:

dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]

转移方程 :

主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同 

如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;

如果text1[i - 1] 与 text2[j - 1]不相同,虽然不能直接递增一位,但是两边字符串都增加了一位,这带来了新的可能性,我们不能从 text1[0,i - 2] 与 text2[0,j - 2]得到结果,,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。

 其实也就相当于,如果我们不能从dp[i-][j-1]推出dp[i][j],那就从dp[i-1][j]和dp[j-1][i]中想办法,因为dp[i-1][j]和dp[j-1][i]时对称的,我们就选择其中的更大值,如下图:

数据结构与算法-动态规划(基础框架+子序列问题)_第25张图片

代码如下:

                if(text1[i-1] == text2[j-1]){
                    dp[i][j] = max(dp[i][j],dp[i-1][j-1]+1);
                }
                else{
                    dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
                }

初始化:
 同样需要第一排和第一列的初值,且初始化都为0

 确定遍历顺序:
从前向后,从上到下,进行两层循环

 完整代码:

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        if(text1.size() == 0 || text2.size() ==0)return 0;
        vector> dp(text1.size()+1,vector(text2.size()+1,0));
    
        for(int i=1;i<=text1.size();i++)
            for(int j =1;j<=text2.size();j++){
                if(text1[i-1] == text2[j-1]){
                    dp[i][j] = max(dp[i][j],dp[i-1][j-1]+1);
                }
                else{
                    dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
                }
            }
        return dp[text1.size()][text2.size()];     
    }

};

这样的dp定义方式有一个优势,结果不用遍历dp数组

3.2.4 不相交的线 

数据结构与算法-动态规划(基础框架+子序列问题)_第26张图片

本题就是寻找最长的公共子序列

数据结构与算法-动态规划(基础框架+子序列问题)_第27张图片

代码如下:

class Solution {
public:
    int maxUncrossedLines(vector& nums1, vector& nums2) {
        vector> dp(nums1.size()+1,vector(nums2.size()+1,0));
        for(int i = 1;i <= nums1.size();i++)
            for(int j = 1 ;j <= nums2.size();j++){
                if(nums1[i-1] == nums2[j-1]){
                    dp[i][j] = max(dp[i-1][j-1]+1,dp[i][j]);
                }else{
                    dp[i][j] = max(dp[i][j-1],dp[i-1][j]);
                }
            }
        return dp[nums1.size()][nums2.size()];
    }
};

 3.2.5 判断子序列

数据结构与算法-动态规划(基础框架+子序列问题)_第28张图片

 此题和其他题的区别在于,返回值位bool,因此会很容易将dp数组的值定义为bool型,但这样初始化很复杂,时间复杂度有点高,初始化如下:

数据结构与算法-动态规划(基础框架+子序列问题)_第29张图片

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int n = s.size(),m = t.size();
        vector> dp(n+1,vector(m+1));
        for(int i = 0;i <= n;i++){
            dp[i][0] = false;
        }
        for(int j = 0;j <= m;j++){
            dp[0][j] = true;
        }
        for(int i = 1;i <= n;i++){
            for(int j = 1;j <= m;j++){
                if(s[i-1] == t[j-1])
                    dp[i][j] = dp[i-1][j-1];
                else{
                    dp[i][j] = dp[i][j-1];
                }
            }
        }
        return dp[n][m];
    }
};

因此我们选择dp[i][j]的含义为,有多少个匹配的字符,当匹配字符==s.size()时,就是true 

数据结构与算法-动态规划(基础框架+子序列问题)_第30张图片

完整代码如下:

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int n = s.size(),m = t.size();
        vector> dp(n+1,vector(m+1,0));
        for(int i = 1;i <= n;i++){
            for(int j = 1;j <= m;j++){
                if(s[i-1] == t[j-1])
                    dp[i][j] = dp[i-1][j-1]+1;
                else{
                    dp[i][j] = dp[i][j-1];
                }
            }
        }
        return dp[n][m] == s.size() ? true : false;
    }
};

3.2.6 不同的子序列-双序列中只改变单序列的典例

数据结构与算法-动态规划(基础框架+子序列问题)_第31张图片

(1)dp定义:
 dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。

(2)转移方程:
此题与其他题不一样,因为是求有多少种删除方法,所以dp数组的更新不能是简单的+1

这里是求总的方法数,那当 s[i-1]==t[j-1]时:

我们不能只考虑将 s[i-1] 和 t[j-1] 同时留下,也即是dp[i][j] =dp[i-1][j-1] (最后一位相互抵消了)

还要考虑,如果前面也有一位 s[k] == s[i-1] (且相对位置满足t的要求),那么我们可以同时留下 s[k] 和 t[j-1] ,将 s[i-1] 删掉,因此dp[i][j] = dp[i-1][j](如果前面不存在s[k],那么此处为0,结果不变)

因为这两种情况时分开讨论的,不相交,因此当s[i-1]==t[j-1]时:

dp[i][j] = dp[i-1][j-1] + dp[i-1][j];

而当s[i-1]!=t[j-1]时:

我们考虑将s[i-1]删除,因为不相等用不到

但是不能删除t[j-1],因为我们在求d[i][j],j对应的就是t[0,j-1],要是将t[j-1]去除,那就不能称为dp[i][j]了(

因为t是子串不能更改),所以不存在dp[i][j-1]和dp[i-1][j-1]两种情况

因此转移方程如下:

                if(s[i-1] == t[j-1])
                    dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
                else{
                    dp[i][j] = dp[i-1][j];
                }

(3)初始化:

以样例为参考:
数据结构与算法-动态规划(基础框架+子序列问题)_第32张图片

代码如下:

        vector> dp(n+1,vector(m+1,0));
        for(int i = 0;i <= n;i++)
            dp[i][0] = 1;

(4)遍历顺序

两层for循环即可

数据结构与算法-动态规划(基础框架+子序列问题)_第33张图片

完整代码如下:

class Solution {
public:
    int numDistinct(string s, string t) {
        int n = s.size(),m = t.size();
        vector> dp(n+1,vector(m+1,0));
        for(int i = 0;i <= n;i++)
            dp[i][0] = 1;
        for(int i = 1;i <= n;i++)
            for(int j = 1;j <= m;j++){
                if(s[i-1] == t[j-1])
                    dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
                else{
                    dp[i][j] = dp[i-1][j];
                }
                // dp[i][j] = dp[i][j] % (1000000007);
            }
        return dp[n][m];
    }
};

3.2.7 两个字符串的删除操作

数据结构与算法-动态规划(基础框架+子序列问题)_第34张图片

编辑距离的简化版

(1) dp定义:
不要连续就,定义dp[i][j]表示word1[0,i-1],word2[0,j-1]的最小步数

(2)转移方程

和编辑距离类似

word1[i-1] == word2[j-1]时,不删除,dp[i][j] = dp[i-1][j-1];

word1[i-1] != word2[j-1]时,在三种删除中选择最小的

                if(word1[i-1] == word2[j-1])
                    dp[i][j] = dp[i-1][j-1];
                else{
                    dp[i][j] = min(
                        dp[i-1][j-1]+2,
                        dp[i-1][j]+1,
                        dp[i][j-1]+1
                    );

 (3)初始化

根据样例进行如下初始化:

数据结构与算法-动态规划(基础框架+子序列问题)_第35张图片

代码:

        vector> dp(n+1,vector(m+1));
        
        for(int i = 0;i <= n;i++)dp[i][0] = i;
        for(int i = 0;i <= m;i++)dp[0][i] = i;

(4)遍历顺序 

数据结构与算法-动态规划(基础框架+子序列问题)_第36张图片

完整代码:

class Solution {
public:
    int min(int a,int b,int c){
        return std::min(std::min(a,b),c);
    }

    int minDistance(string word1, string word2){
        int n = word1.size(),m = word2.size();
        vector> dp(n+1,vector(m+1));
        
        for(int i = 0;i <= n;i++)dp[i][0] = i;
        for(int i = 0;i <= m;i++)dp[0][i] = i;

        for(int i = 1;i <= n;i++)
            for(int j = 1 ;j <= m; j++){
                if(word1[i-1] == word2[j-1])
                    dp[i][j] = dp[i-1][j-1];
                else{
                    dp[i][j] = min(
                        dp[i-1][j-1]+2,
                        dp[i-1][j]+1,
                        dp[i][j-1]+1
                    );
                }
            }
        return dp[n][m];
    }
};

3.2.8 两个字符串的最小ascii删除和

数据结构与算法-动态规划(基础框架+子序列问题)_第37张图片 和上一题及其类似,只是初始化和更新转移方程的形式有点区别,不多赘述

代码如下:

class Solution {
public:

    int min(int a,int b,int c){
        return std::min(std::min(a,b),c);
    }

    int minimumDeleteSum(string s1, string s2){
        int n = s1.size(),m = s2.size();
        vector> dp(n+1,vector(m+1));
        
        for(int i = 0;i <= n;i++)
            for(int j = 0;j < i;j++)
                dp[i][0] += int(s1[j]);
        for(int i = 0;i <= m;i++)
            for(int j = 0;j < i;j++)
                dp[0][i] += int(s2[j]);

        for(int i = 1;i <= n; i++)
            for(int j = 1 ;j <= m; j++){
                if(s1[i-1] == s2[j-1])
                    dp[i][j] = dp[i-1][j-1];
                else{
                    dp[i][j] = min(
                        dp[i-1][j-1]+int(s1[i-1])+int(s2[j-1]),
                        dp[i-1][j]+int(s1[i-1]),
                        dp[i][j-1]+int(s2[j-1])
                    );
                }
            }
        return dp[n][m];
    }
};

3.3 回文子串和子序列

(1)回文子串要求连续,可以用双指针进行解答;

(2)但是回文子序列不连续,双指针失效,可以复制一份然后,将其倒转,使用双序列的方法进行解答.

3.3.1 回文子串

 数据结构与算法-动态规划(基础框架+子序列问题)_第38张图片

使用双指针解法,相比dp解法时间复杂度更低 

代码如下,细节参考数组一章,双指针部分:

class Solution {
public:
    int jud(string s,int l,int r){
        int count = 0;
        while(l >=0 && r 

3.3.2 最大回文子序列 

数据结构与算法-动态规划(基础框架+子序列问题)_第39张图片

 将s复制一份再倒转,然后对两个序列求最大公共子序列,代码如下:

//复制一份,倒转,求最大公共子序列
class Solution {
public:
    int longestPalindromeSubseq(string s) {
        string t = s;
        reverse(s.begin(),s.end());
        int n = s.size();
        vector> dp(n+1,vector(n+1,0));
        for(int i = 1;i <= n;i++)
            for(int j = 1;j <= n;j++){
                if(s[i-1]==t[j-1])dp[i][j] = dp[i-1][j-1] + 1;
                else{
                    dp[i][j] = max(max(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1]);
                }
            }
        return dp[n][n];
    }
};

你可能感兴趣的:(动态规划,算法)