• 代码随想录52——动态规划:300最长递增子序列、674最长连续递增序列、 718最长重复子数组


    1.300最长递增子序列

    参考:代码随想录,300最长递增子序列力扣题目链接

    1.1.题目

    在这里插入图片描述

    1.2.解答

    最长上升子序列是动规的经典题目,这里dp[i]是可以根据dp[j] (j < i)推导出来的,那么依然用动规五部曲来分析详细一波:

    1.dp[i]的定义

    dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度

    2.状态转移方程

    位置i的最长升序子序列等于 j从0到i-1各个位置的最长升序子序列 + 1 的最大值。

    所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);

    注意这里不是要 dp[i]dp[j] + 1 进行比较,而是我们要取dp[j] + 1的最大值,这个是通过for循环来实现的。

    3.dp[i]的初始化

    每一个i,对应的dp[i](即最长上升子序列)起始大小至少都是1.

    4.确定遍历顺序

    dp[i] 是有0到i-1各个位置的最长升序子序列 推导而来,那么遍历i一定是从前向后遍历。

    j其实就是0到i-1,遍历i的循环在外层,遍历j则在内层,代码如下:

    for (int i = 1; i < nums.size(); i++) {
        for (int j = 0; j < i; j++) {
            if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
        }
        if (dp[i] > result) result = dp[i]; // 取长的子序列
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    5.举例推导dp数组

    输入:[0,1,0,3,2],dp数组的变化如下

    在这里插入图片描述
    直接给出代码如下:

    int lengthOfLIS(vector<int> &nums)
    {
        if(nums.size() <= 1)
            return nums.size();
        int result = 0;  // 最终结果
    
        // 1.定义dp数组
        vector<int> dp(nums.size(), 1);
    
        // 2.动态规划,计算前面的每一个子串的最长序列
        for(int i = 1; i < nums.size(); i++)
        {
            // 对这个子串,计算它前面的所有子串的最大长度
            for(int j = 0; j < i; j++)
            {
                // 当前位置元素 > 子串的最后一个元素,则递增序列长度可以+1
                if(nums[i] > nums[j])
                {
                    dp[i] = max(dp[i], dp[j] + 1);  // 和上一次的比,取最大值
                }
            }
            // 找到了以i结尾的子串的最大递增长度,求最终的最大递增长度
            if(dp[i] > result)
                result = dp[i];
        } 
    
        return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    注意:这道题目感觉很简单,但是感觉还是没有完全理解。注意以下两点:

    • 遍历i结束的子串的最大递增长度的时候,需要从前面的所有子串的最大递增长度中挨个对比,找到最大的递增长度
    • 最终要的是最大递增的子序列,所以并没有要求整个数组的最大递增序列,也就是不要求最后结尾的字符一定是数组的最后一个字符,因此最终结果并不是dp.back(),而是要在dp数组中寻找最大值。所以在代码中for(i)的循环中就一直在用result记录最大值。

    注意:这道题目自己的疑问可能就是在于,为什么在当前i的位置找最长的递增序列的时候,遍历i之前的所有子串j,然后nums[i] > nums[j]的话,就可以把最长的递增序列+1呢?有可能会nums[j]并不是递增的数啊,也就是0-j的递增的子序列长度和j无关,是0 - (j-1)的子序列的递增长度,所以现在如果nums[i] > nums[j]的话,未必可以保证nums[i]也是相比0 - (j-1)的子序列递增的啊?

    解答:其实是因为前面自己对dp数组的理解出现了问题!(不过代码随想录中也没说清楚,他说的是dp[i]是i之前包括i的以nums[i]结尾最长上升子序列的长度)。其实这里的dp数组虽然仍然是上升子序列,但是要求这个子序列必须是以nums[i]为结尾的,也就是这个递增子序列中必须有nums[i]!所以说,只要nums[i] > nums[j],由于dp[j]是以nums[j]为结尾的最长递增子序列的长度,所以现在nums[i] > nums[j]之后,那么以nums[i]为结尾的最长递增子序列的长度就一定是dp[j]+1。这样我们把所有的nums[i]结尾的子序列遍历完成之后,最长的子序列就是这些子序列的最长长度中的最大值!

    如下所示,数组和以他们为结尾的最长子序列的长度。所以最后的结果就是从所有的最长递增子序列中寻找一个最大的值,也就是寻找dp[i]数组中的最大值,这也就是代码中if(dp[i] > result) result = dp[i]的作用,就是在寻找dp数组中的最大值。因此,dp数组的含义是:nums[i]为结尾、必须包含nums[i]的最长递增子序列的长度

    在这里插入图片描述
    注意:如下的理解是错误的,就是认为dp[i]是从0-i的子序列的最大递增长度,未必包含nums[i]。这样的话问题就出现在无法使用递推公式进行递推了。比如nums[0] - nums[4]的最大递增长度是5,现在nums[5] > nums[4],但是怎么知道nums[0] - nums[5]的最大递增长度呢?没法知道,关键因素就是如果按照这样定义dp数组,最大递增长度中不一定含有nums[i],这样就导致没法判断最长递增序列的最后一个数是多少了,所以就没法对比了。

    2.674最长连续递增序列

    参考:代码随想录,674最长连续递增序列力扣题目链接

    2.1.题目

    在这里插入图片描述

    2.2.解答

    本题相对于 动态规划:300.最长递增子序列 最大的区别在于“连续”。

    动规五部曲分析如下:

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

    dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]注意:和昨天的题目一样,这个递增序列中一定是包含nums[i],这样后面才能使用递推公式,原因分析见上一题的最后自己写的笔记。

    注意这里的定义,一定是以下标i为结尾并不是说一定以下标0为起始位置注意:这里不一定以nums[0]为起始位置很重要,比如1, 2, 5, 3, 4,遍历到3的时候发现不是连续递增了,所以就又重新从1开始计算连续递增子序列长度了,然后到4的时候以4结尾的连续递增子序列长度变为2。

    2.确定递推公式

    如果 nums[i + 1] > nums[i],那么以i+1 为结尾的数组的连续递增的子序列长度 一定等于 以i为结尾的数组的连续递增的子序列长度 + 1 。即:dp[i + 1] = dp[i] + 1;

    注意这里就体现出和动态规划:300.最长递增子序列的区别!因为本题要求连续递增子序列,所以就必要比较nums[i + 1]与nums[i],而不用去比较nums[j]nums[i] (j是在0到i之间遍历)。

    注意:这里也可以借此机会再次好好理解昨天的题目。昨天的题目因为不要求是连续的子序列,所以当前的nums[i]就要和0 - (i-1)结尾的所有子序列的最后一个数进行比较,如果nums[i] > nums[j],那么就找到了一个以nums[i]结尾的递增子序列,就可以计算这个递增子序列的长度;然后遍历所有的0 - (i-1)结尾的子序列,最后就可以找到以nums[i]结尾的最长递增子序列的长度。

    而本题中,由于要求必须是连续递增的子序列,所以不需要遍历之前以0 - (i-1)结尾的所有子序列了,只需要遍历nums[i-1]结尾的子序列即可

    3.dp数组如何初始化

    以下标i为结尾的数组的连续递增的子序列长度最少也应该是1,即就是nums[i]这一个元素,所以dp[i]应该初始1。

    4.确定遍历顺序

    从递推公式上可以看出, dp[i + 1]依赖dp[i],所以一定是从前向后遍历。

    本文在确定递推公式的时候也说明了为什么本题只需要一层for循环,代码如下:

    for (int i = 0; i < nums.size() - 1; i++) {
        if (nums[i + 1] > nums[i]) { // 连续记录
            dp[i + 1] = dp[i] + 1; // 递推公式
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    5.举例推导dp数组

    已输入nums = [1,3,5,4,7]为例,dp数组状态如下:在这里插入图片描述
    最后给出代码如下,很简单:

    int findLengthOfLCIS(vector<int> &nums)
    {
        if(nums.size() <= 1)
            return nums.size();
        
        int result = 0;  // 最终结果
    
        // 1.定义并初始化dp数组
        vector<int> dp(nums.size(), 1);
    
        // 2.动态规划开始递推
        for(int i = 1; i < nums.size(); i++)
        {
            if(nums[i] > nums[i-1])
            {
                dp[i] = dp[i-1] + 1;  // 如果递增,则递增长度+1
            }
            else
            { }// 否则什么也不干,即重新开始一个新的递增序列,递增子序列长度重新初始化成1
    
            // 记录最终的最长连续递增子序列
            result = dp[i] > result ? dp[i] : result;
        }
    
        return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    3.718最长重复子数组

    参考:代码随想录,718最长重复子数组力扣题目链接

    3.1.题目

    在这里插入图片描述

    3.2.解答

    动归五部曲

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

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

    此时细心的同学应该发现,那dp[0][0]是什么含义呢?总不能是以下标-1为结尾的A数组吧。

    其实这个地方主要是为了后面的递推公式计算的方便,把dp[0][j]dp[i][0],即第0行、第0列都初始化成0,方便递推公式计算,后面看dp数组的状态图就明白了。

    2.确定递推公式

    根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。

    即当A[i - 1] B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;

    根据递推公式可以看出,遍历i 和 j 要从1开始!

    3.dp数组如何初始化

    根据dp[i][j]的定义,dp[i][0]dp[0][j]其实都是没有意义的!

    dp[i][0]dp[0][j]要初始值,因为 为了方便递归公式dp[i][j] = dp[i - 1][j - 1] + 1;,所以dp[i][0] 和dp[0][j]初始化为0

    举个例子A[0]如果和B[0]相同的话,dp[1][1] = dp[0][0] + 1,只有dp[0][0]初始为0,正好符合递推公式逐步累加起来。

    4.确定遍历顺序

    外层for循环遍历A,内层for循环遍历B。或者外层for循环遍历B,内层for循环遍历A。这俩没有什么区别,结果都是一样的。

    同时题目要求长度最长的子数组的长度。所以在遍历的时候顺便把dp[i][j]的最大值记录下来

    代码如下:

    for (int i = 1; i <= A.size(); i++) {
        for (int j = 1; j <= B.size(); j++) {
            if (A[i - 1] == B[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            }
            if (dp[i][j] > result) result = dp[i][j];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    5.举例推导dp数组

    拿示例1中,A: [1,2,3,2,1],B: [3,2,1,4,7]为例,画一个dp数组的状态变化,如下:
    在这里插入图片描述
    最后给出代码如下,注意如下代码中自己做了一些调整, 主要是i/j的意思。上面代码随想录的i/j指的是dp数组中的索引,由于dp数组为了方便递推公式增加了第0行和第0列,这样对应nums中的索引就是i-1/j-1;下面自己写的代码中i/j值得就是nums中的索引,由于dp数组为了方便递推公式增加了第0行和第0列,所以对应dp数组中的 索引是i+1/j+1

    int findLength(vector<int> &nums1, vector<int> &nums2)
    {
        if(nums1.empty() || nums2.empty())
            return 0;
    
        int result = 0;   // 记录最终结果
    
        // 1.定义并初始化dp数组
        vector<vector<int>> dp(nums1.size()+1, vector<int>(nums2.size()+1, 0));
    
        // 2.动态规划:类似背包问题,开始递推
        for(int i = 0; i < nums1.size(); i++)
        {
            for(int j = 0; j < nums2.size(); j++)
            {
                if(nums1[i] == nums2[j])
                {
                    // 注意这里dp[0][j]和dp[i][0]都初始化成0,没有物理意义,只是为了方便递推公式计算
                    dp[i+1][j+1] = dp[i][j] + 1;
                }
    
                // 更新最大长度
                result = dp[i+1][j+1] > result ? dp[i+1][j+1] : result;  
            }  
        }
    
        return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
  • 相关阅读:
    【Python GUI编程】零基础也能轻松掌握的学习路线与参考资料
    36基于matlab的对分解层数和惩罚因子进行优化
    DataFrame API入门操作及代码展示
    是时候给大家介绍 Spring Boot/Cloud 背后豪华的研发团队了。
    【从头构筑C#知识体系】2.2 匿名方法
    用于准确量化颅面对称性和面部生长的 3D 头影测量方案(Matlab代码实现)
    怎么避免电脑数据被拷贝?电脑如何禁用USB功能?
    tiup dm restart
    .Net Core Aop之IResourceFilter
    bean无法被注入的常见问题和springboot的三种扫描并加载Bean方式
  • 原文地址:https://blog.csdn.net/qq_42731705/article/details/127818264