动态规划——子序列问题——连续?不连续?

子序列问题是动态规划的一个常考题型,本篇文章主要介绍 子序列(连续)和子序列(不连续)两个问题。连续和不连续指的是子序列是否是连续的,还是说中间是可以有间隔的子序列。

一:子序列(不连续)

300. 最长递增子序列

 

 

分析:从题意我们可以读出该题想求的是不连续的递增子序列。

解法:动态规划

第一步:dp[i]的定义

dp[i]表示i之前包括i的最长上升子序列的长度。

第二步:状态转移方程

位置 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的最大值

第三步:dp[i]的初始化

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

第四步:确定遍历顺序

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

j 其实就是 0 到 i-1,遍历i的循环在外层,遍历j则在内层。

代码如下:

class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp,1);
        int result = 1;
        for(int i = 1 ; i < nums.length ; i++){
            for(int j = 0 ; j < i ; j++){
                if(nums[i]>nums[j]){
                    dp[i] = Math.max(dp[i],dp[j]+1);
                }
            }
            if(dp[i] > result){
                result = dp[i];
            }
        }
        return result;
    }
}

 1143. 最长公共子序列

 

 

分析:

第一步:确定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 - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。

即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

第三步:dp数组如何初始化

先看看dp[i][0]应该是多少呢?

text1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;

同理dp[0][j]也是0。

其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。

第四步:确定遍历顺序

从递推公式,可以看出,有三个方向可以推出dp[i][j],如图:

 

那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。

代码如下

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int[][] dp= new int[text1.length()+1][text2.length()+1];
        int res = 0;
        for(int i = 1 ; i <= text1.length() ; i++){
            for(int j = 1 ; j <= text2.length() ; j++){
                if(text1.charAt(i-1) == text2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1]+1;
                }else{
                    dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
                }
                if(dp[i][j] > res){
                    res = dp[i][j];
                }
            }
        }
        return res;
    }
}

1035. 不相交的线

 

 

分析:仔细读题会发现其实本题就是求最长公共子序列(不连续)和上题一模一样,换一下参数就可以直接AC。 

代码如下:

class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int[][] dp= new int[nums1.length+1][nums2.length+1];
        int res = 0;
        for(int i = 1 ; i <= nums1.length ; i++){
            for(int j = 1 ; j <= nums2.length ; j++){
                if(nums1[i-1] == nums2[j-1]){
                    dp[i][j] = dp[i-1][j-1]+1;
                }else{
                    dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
                }
                if(dp[i][j] > res){
                    res = dp[i][j];
                }
            }
        }
        return res;
    }
}

二:子序列(连续)

674. 最长连续递增序列

 

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

dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]

注意这里的定义,一定是以下标i为结尾,并不是说一定以下标0为起始位置。

第二步:确定递推公式

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

即:dp[i] = dp[i-1] + 1;

因为本题要求连续递增子序列,所以就必要比较nums[i + 1]与nums[i],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历)。

既然不用j了,那么也不用两层for循环,本题一层for循环就行,比较nums[i] 和 nums[i-1]。

第三步:dp数组如何初始化

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

所以dp[i]应该初始1;

第四步:确定遍历顺序

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

代码如下:

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int dp[] = new int[nums.length];
        Arrays.fill(dp,1);
        int res = 1;
        for(int i = 1 ; i < nums.length ; i++){
            if(nums[i] > nums[i-1]){
                dp[i] = dp[i-1]+1;
            }
            if(dp[i] > res){
                res = dp[i];
            }
        }
        return res;
    }
}

 718. 最长重复子数组

分析:

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

dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。

第二步:确定递推公式

根据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开始!

第三步: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。

第四步:确定遍历顺序

外层for循环遍历A,内层for循环遍历B。或者相反都可以。

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

代码如下:

class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int res = 0 ;
        int[][] dp = new int[nums1.length+1][nums2.length+1];
        for(int i = 1 ; i <= nums1.length ; i++){
            for(int j = 1 ; j <= nums2.length ; j++){
                if(nums1[i-1] == nums2[j-1]){
                    dp[i][j] = dp[i-1][j-1] + 1;
                    if(dp[i][j] > res){
                        res = dp[i][j];
                    }
                }
            }
        }
        return res;
    }
}

总结:这道题我觉得和子序列(不连续)里面的第二题非常像,只不过这道题要求必须是联系的,所以只有在 nums1[i-1] == nums2[j-1] 时,dp[i][j] == dp[i-1][j-1] + 1,如果不相等就无需记录了,默认值1即可。但是上面的第二题,还需要比较dp[i][j-1] 和 dp[i-1][j] 的最大值进行记录,因为之后如果有相等的还需要前面的计算结果。而本题一旦不相等,这个子数组就断了,无需记录之前的数据。

53. 最大子数组和

分析:这个题之前用贪心也写过,但是这里还是使用动态规划来进行解决,下面我会贴上贪心的代码。

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

dp[i]:包括下标i之前的最大连续子序列和为dp[i]

第二步:确定递推公式

dp[i]只有两个方向可以推出来:

  • dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
  • nums[i],即:从头开始计算当前连续子序列和

一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);

第三步:dp数组如何初始化

从递推公式可以看出来dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础。

dp[0]应该是多少呢?

根据dp[i]的定义,很明显dp[0]应为nums[0]即dp[0] = nums[0]。

第四步:确定遍历顺序

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

 动态规划代码如下:

class Solution {
    public int maxSubArray(int[] nums) {
        int dp[] = new int[nums.length];
        int res = nums[0];
        dp[0] = nums[0];
        for(int i = 1 ; i < nums.length ; i++){
            dp[i] = Math.max(dp[i-1]+nums[i],nums[i]);
            if(dp[i]>res){
                res = dp[i];
            }
        }
        return res;
    }
}

贪心代码如下:

class Solution {
    public int maxSubArray(int[] nums) {    
        int max = Integer.MIN_VALUE;
        int sum = 0;
        for(int i = 0 ; i < nums.length ; i++){
            sum += nums[i];
            if(sum > max){
                max = sum;
            }
            if(sum < 0){
                sum = 0;
            }
        }
        return max;
    }
}

总结:动态规划在解决子序列问题这里有两把刷子,以后遇到子序列的问题都可以考虑一下使用动态规划来进行解决。

如果有写的不对的地方,欢迎指出。感谢代码随想录。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐