代码随想录算法训练营第43天 | LeetCode300.最长递增子序列、LeetCode674.最长连续递增子序列、LeetCode718.最长重复子数组

目录

LeetCode300.最长递增子序列

LeetCode674.最长连续递增子序列 

1. 贪心算法

2. 动态规划

LeetCode718.最长重复子数组


LeetCode300.最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 

思路:最长递增子序列,这道题的难点在于如何能够处理不连续的,但是还是递增的序列。

或者换句话说,如果递增子序列不是连续的,我如何能够找到这样一段满足题意得递增子序列,并在循环结束后,返回最大长度。

所以这里我们对于求dp[i]很自然的就会想到是由前一个状态推导出来的,也就是dp[j]+1,当然是在满足nums[i]>nums[j]的情况下有这样的递推公式。

这里我们要注意了,dp[j]里面的j的范围,不仅仅只是i-1(也就是说不只是i-1这个相邻的前一个状态),还应该包括前面的所有状态的值,都应该拿nums[i]与它们做比较,这样dp[i]最后的值就会是最长的递增子序列的长度。

所以想要求dp[i],就要拿nums[i]与nums[j],j的范围是0到i-1,只要满足nums[i]>nums[j],就有dp[i]=dp[j]+1,当然这样的更新可能存在不止一次,那就不断更新,只要每次取的是最大值,那么就能保证最后返回的结果也是最大值。

    int lengthOfLIS(vector<int>& nums) {
        if(nums.size() <= 1) return nums.size();
        vector<int> dp(nums.size(), 1);//dp[i]表示前i个元素最长子序列的长度
        int result = 0;
        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);//这里是取第i个元素的前面的j元素中的dp值最大的,j的取值范围为0到i-1
                }
                if(dp[i] > result) result = dp[i];
            }
        }
        return result;
    }

时间复杂度:O(n^2)

空间复杂度:O(n)

LeetCode674.最长连续递增子序列 

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 lrl < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

思路:将上面的题做过后,再来做这道题就非常简单了。

这里的最长递增子序列是连续的,所以只需要在求dp[i]时,只考虑dp[i-1]对它的影响。

题的复杂度一下就减少了很多。

这里介绍两种方法求解。

1. 贪心算法

使用贪心来做的话注意两点,count在nums[i]>nums[i-1]的时候会加1,一旦遇到不满足情况的,就会重新开始计数,这比较关键。

    int findLengthOfLCIS(vector<int>& nums) {
        if(nums.size() <= 1) return nums.size();
        int count = 1;//初始的子序列长度为1
        int result = 0;//记录最终结果
        for(int i = 1; i < nums.size(); i ++){
            if(nums[i] > nums[i - 1]) count ++;//当满足连续递增条件的话count加1
            else count = 1;//当不满足的时候count重新开始计数
            if(count > result) result = count;//及时更新最长子序列长度
        }
        return result;
    }

时间复杂度:O(n)

空间复杂度:O(1)

2. 动态规划

动态规划的话其实有了上一题的基础,在满足题意的情况下dp[i]=dp[i-1]+1,然后及时更新最终结果,最后返回即可。

整体还是比较好理解的。

    int findLengthOfLCIS(vector<int>& nums) {
        if(nums.size() <= 1) return nums.size();
        vector<int> dp(nums.size(), 1);//dp[i]表示前面i个数中的最长子序列长度
        int result = 0;
        for(int i = 1; i < nums.size(); i ++){
            if(nums[i] > nums[i - 1]) dp[i] = dp[i - 1] + 1;
            if(dp[i] > result) result = dp[i]; 
        }
        return result;      
    }

时间复杂度:O(n)

空间复杂度:O(n)

LeetCode718.最长重复子数组

给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。 

思路:这里比上面的稍微要难一点。

因为前面是在一个数组上进行操作,这里变成了两个数组。

所以我们这里使用了dp[i][j]表示以下标i-1结束的nums1,以及以下标j-1结束的nums2的最长子数组长度。

我们可以很容易知道,dp[i][j]与dp[i-1][j-1]的状态有关,如果说nums1[i-1]等于了nums[j-1],那么就会有dp[i-1][j-1](以下标i-2结束的nums1,以及以下标j-2结束的nums2的最长子数组长度)加上1,所以其实也比较好做了。

上面递推公式中有dp[i-1][j-1],那么循环下标就得从1开始了,但是下标为0的地方需要进行初始化,因为后面会用到,那么结合具体情景来看的话,如果说nums1[0]等于了nums2[0],那么dp[1][1]=dp[0][0]+1,所以这里dp[0][0]初始化为0合适,因为这样的话刚好最长子数组为1。

    int findLength(vector<int>& nums1, vector<int>& nums2) {
        //dp[i][j]表示对于以下标i-1结尾的nums1,和以下标j-1结尾的nums2的最长子数组长度
        //注意这里的相同的子数组是求的连续的,不是非连续的
        vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
        int result = 0;
        for(int i = 1; i < nums1.size() + 1; i ++){
            for(int j = 1; j < nums2.size() + 1; j ++){
                if(nums1[i - 1] == nums2[j - 1]){
                    dp[i][j] = dp[i - 1][j - 1] + 1;//当nums1[i-1]与nums2[j-1]相等时,就等于dp[i-1][j-1]的基础上加1
                }
                if(dp[i][j] > result) result = dp[i][j];//及时记录下最大数组长度
            }
        }
        return result;
    }

时间复杂度:O(n*m)

空间复杂度:O(n*m)

当然还可以使用一维滚动数组来实现上面的代码,进行空间消耗的一个优化。

这里需要注意的有两点

一是对于这个一维数组的更新的遍历顺序需要从后往前遍历,因为dp[j]的更新会受到dp[j-1]状态的影响,如果正向遍历,会造成一些值重复叠加,显然结果是不对的,因此从后往前遍历就是为了避免重复累加;

二是在nums1[i-1]与nums2[j-1]不相等时,需要及时更新dp[j]为0,因为这是一个一维数组,如果不更新为0,保留了之前的状态,那么就会在后面结算的时候出现使用dp[j-1]更新dp[j]的时候出现错误。本身如果不相等,那么说明最长子数组长度就该为0,对于一维数组来说,状态值是不断在更新,不得不进行一些值的改变,因为相同位置的值在下一次到达时可能就会覆盖成新的值;而相较于二维数组来说,不存在覆盖一说,每个地方都存储了之前的值,空间也足够,所以不需要覆盖。

    int findLength(vector<int>& nums1, vector<int>& nums2) {
        //使用滚动一维数组来记录并且不断更新状态
        vector<int> dp(nums2.size() + 1, 0);
        int result = 0;
        for(int i = 1; i < nums1.size() + 1; i ++){
            for(int j = nums2.size(); j > 0; j --){//注意这里的顺序是倒序,防止重复叠加
                if(nums1[i - 1] == nums2[j - 1]){
                    dp[j] = dp[j - 1] + 1;
                }else{
                    dp[j] = 0;//这里在不匹配的时候记得重置为0,否则可能会出现错误答案
                }
                if(dp[j] > result) result = dp[j];//及时更新dp[j]
            }
        }
        return result;
    }

时间复杂度:O(n*m)

空间复杂度:O(m)

当然了,还可以将dp[i][j]表示以下标i结束的nums1,以及以下标j结束的nums2的最长子数组长度。

这样也是可以的,但是需要进行额外的初始化操作,比如nums1[0]与nums2[j]相等了,那么最大子数组长度就该初始化为1,同理对于nums1[i]与nums2[0]相等的时候也是这样的。

还需要注意的是for循环的遍历一定要从i=0,j=0开始,不能从i=1,j=1开始。因为一旦这样开始,就会导致result没有办法更新所有的情况存在的状态值,导致最后返回的是个错误值。详细的解释可以看一看下面代码中注释的地方,这样就应该能明白为什么一定要使得i、j都从0开始了。

    int findLength(vector<int>& nums1, vector<int>& nums2) {
        //这里使用dp[i][j]表示以下标i结尾的nums1,和以下标j结尾的nums2的最大子数组长度
        vector<vector<int>> dp(nums1.size(), vector<int>(nums2.size(), 0));
        //初始化,对dp[i][0]以及dp[0][j]的状态值进行判断并且初始化
        for(int i = 0; i < nums1.size(); i ++) if(nums1[i] == nums2[0]) dp[i][0] = 1;
        for(int j = 0; j < nums2.size(); j ++) if(nums2[j] == nums1[0]) dp[0][j] = 1;

        int result = 0;//记录最终结果
        for(int i = 0; i < nums1.size(); i ++){
            for(int j = 0; j < nums2.size(); j ++){
                //注意这里的判断是需要将i=0以及j=0排除掉的
                //那为什么不直接使i、j都从1开始呢,这样就不用判断了,
                //因为如果从1开始,就会使得result不能将所有结果全部更新,会跳过i=0或者j=0时的最长子数组长度
                //比如直观的一个例子就是假设有这样两个数组[1,2,3,4],[1,5,6,7]
                //从1开始的话,最后的result为0,显然正确答案应该是1,出现这样的状况就是因为跳过了使用i=0,j=0的状态对result更新的情况,最终导致结果的错误
                //当然还有可能是这样的数组[1,2,3,2,8],[5,6,1,4,7],都是应该使得i=0或者j=0,但是贸然将i和j都从1开始所造成的情况考虑不周全的状况
                //因此这里必然是从0开始,虽然第一个判断进不去,但是可以进入第二个循环,尝试更新result
                if(nums1[i] == nums2[j] && i >0 && j > 0){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                if(dp[i][j] > result) result = dp[i][j];
            }
        }
        return result;
    }

时间复杂度:O(n*m)

空间复杂度:O(n*m)

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值