对于子串和子序列动态规划的一些感悟

写在开头

对于dp数组,无论是一维还是二维,某一个位置表示的要么就是以该位置为结尾(必须包含该位置的元素)的子问题的答案,这种方式一般最后的位置即为最终答案;要么是以该位置就为结尾的的子问题答案,这种方式一般最后需要扫描整个数组找到答案。


下面举四个例子:

可以看出5种问题可以分为两类,一类是连续(子数组,连续序列=子串),另一类是不连续(子序列)

连续问题

1、连续子数组最大和

问题描述:

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
示例1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

思路解析:

一维dp数组,因为需要连续,所以每个位置是以自己为结尾的子问题的答案,即以自己未结束的子数组的最大和,该子数组因为要包含nums[i]这个元素,nums[i-1]表示的是以 i 位置元素前一个元素结束的子数组的答案,所以nums[i]可以跟在它的子数组的后面形成连续的(也就是子数组)。

状态转移方程为 dp[i] = Math.max(dp[i-1]+nums[i], nums[i]);

//以下代码可以进行优化,只是为了显示框架的相同才这样写
//以下求子数组或者子序列个数的问题都可以对代码进行一定的修改得到对应子数组或者子序列
class Solution {
    public int maxSubArray(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = nums[0];

        for(int i=1;i<nums.length;i++){
            dp[i] = Math.max(dp[i-1]+nums[i],nums[i]);
        }

        int res = Integer.MIN_VALUE;
        for(int i=0;i<dp.length;i++){
            res = Math.max(dp[i], res);
        }
        return res;
    }
}

2、最长连续递增序列

问题描述:

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。
提示:
1 <= nums.length <= 104
-109 <= nums[i] <= 109

思路解析

跟第一题一样,因为是连续序列,其实就是子数组或者字符串的子串,以为dp数组,每个位置仍然是以自己为结尾的子问题的答案,也就是如果nums[i]比nums[i-1]大,则可以将自己并入nums[i-1]所表示的子数组(因为dp[i-1]代表的子数组包括nums[i-1]自身,所以将nums[i]并入之后仍然是连续的)

转台转移方程为 dp[i] = nums[i] > nums[i-1] ? dp[i-1] + 1 : 1

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = 1;
        for(int i = 1; i<nums.length ; i++){
            dp[i] = nums[i] > nums[i-1] ? dp[i-1]+1 : 1;
        }

        int res = Integer.MIN_VALUE;
        for(int i=0;i<dp.length;i++){
            res = Math.max(res,dp[i]);
        }
        return res;
    }
}

3、最长重复子数组

问题描述:

给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度
示例 1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
示例 2:
输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 100

思路解析

找两个数组最长重复子数组,类似张两个字符串的最长公共子串,都是连续问题,使用二维dp数组,每个位置dp[i][j],表示数组nums1的 i 位置元素,以及数组nums2的 j 位置元素为答案最长重复子数组的结尾元素(包含这两个元素)时的长度,当两个字符相等时,可以直接接在nums1[i-1]和nums[j-1]的元素后面,因为dp[i-1][j-1]代表的就是以前者为结尾的重复数组,将nums1[i]和nums2[j]接后面仍然是连续的子数组;当不相等时,以它俩结尾的子数组不可能相等,所以子数组为0

状态转移方程为:dp[i][j] = nums1[i] == nums2[j] ? dp[i-1][j-1] : 0

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

        int res = Integer.MIN_VALUE;
        for(int i = 0; i < nums1.length; i++){
            for(int j = 0; j < nums2.length;j++){
                res = Math.max(dp[i][j], res);
            }
        }

        return res;
    }
}

以上处于连续的问题,子问题是以该位置为结束(子问题答案必须包含该位置元素),所以前一个位置自问的答案必然包括前一个未知元素,所以该位置可以直接结在后面保证连续。

非连续问题

1、最长递增子序列

问题描述:

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104

思路解析:

以为dp数组,dp[i]表示以自己为结尾(无需连续)的子问题的答案.而对于序列问题,由于不需要连续,所以每个dp都需要从头扫描这个给数组,因为nums[i]可能在nums[i-1]后面,也可能在nums[i-2]等等的后面

状态转移方程,dp[i] = Math.max(dp[j]+1,dp[i])

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

    }
}

2、最长公共子数组

问题描述:

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。
示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0 。

思路解析:

需要使用到二维dp数组,dp[i][j]仍然表示以 i 位置元素和 j 位置元素为结尾元素(无需连续)的子问题答案

状态转移方程:dp[i][j] = str1[i] == str2[j] ? dp[i-1][j-1] + 1 : Math.max(dp[i-1][j], dp[i][j-1])

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
       int[][] dp = new int[text1.length()+1][text2.length()+1];
        for(int[] row:dp){
            Arrays.fill(row,0);
        }

        for(int i=1;i<dp.length; i++){
            for (int j = 1; j < dp[i].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]);
                }
            }
        }

        return dp[text1.length()][text2.length()];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值