LeetCode刷题之动态规划(二)

1,200 阅读14分钟

最长递增子序列

300. 最长上升子序列(Medium)

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n^2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

一开始没有考虑清楚,以为可以使用循环遍历的方式来解答,但是做了之后才发现不可行,因为遍历过程中当前选择的元素会影响后面的选择,比如[10,9,2,5,3,4] 输出是2,结果却是3,遍历:2->5,正确结果:2->3->4。

解法一:递归,寻找最长递增子序列的时候,每个位置上的数字有两个选择:选或不选,前面的数字会影响到后面能选择的数字(因为是递增的),这种方法时间复杂度为O(2^n),空间复杂度为O(n^2)。

class Solution{
    public int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0){
            return 0;
        }
        return lengthOfLIS(nums, Integer.MIN_VALUE, 0);
    }

    private int lengthOfLIS(int[] nums, int pre, int cur) {
        //base case
        if (cur == nums.length){
            return 0;
        }
        
        //包含当前元素
        int taken = 0;
        //如果当前数大于pre,taken++
        if (nums[cur] > pre){
            taken = 1 + lengthOfLIS(nums, nums[cur], cur + 1);
        }
        //不包含
        int nottaken = lengthOfLIS(nums, pre, cur + 1);
        return Math.max(taken, nottaken);
    }
}

解法二:将暴力递归转换为动态规划,再明确动态规划是简化暴力递归的方法,优化掉重复计算的部分。老方法:

  • 第一步找变量:cur和result(最长递增子序列)
  • 第二步由递归找出转移方程:dp[i]=Math.max(dp[i],dp[j]+1),dp[i]表示前i个数的最大递增子序列数量
  • 转移方程的意义是指在j属于[0,i],新增i,如果nums[i]>nums[j],则dp[i]=dp[j]+1,否则不变
public class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0){
            return 0;
        }
        int[] dp = new int[nums.length];
        int res = 1;
        //填充dp,最短子序列是1
        Arrays.fill(dp, 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);
                }
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

解法三:贪心思想+二分查找。

  • 基本的思想是:如果前面的数越小,后面接上一个随机数,就会有更大的可能性构成一个更长的“上升子序列”。具体可见LeetCode
  • 准备一个数组tail存储遍历到的最长子序列的尾,初始化为空
  • 遍历nums,如果遇到nums[i]>tail最后一个元素,那么直接添加到其后面
  • 如果nums[i]<tail最后一个元素,在有序数组 tail 中查找第 1 个等于大于 num 的那个数,试图让它变小;
    • 如果有序数组 tail 中存在等于 num 的元素,什么都不做,因为以 num 结尾的最短的“上升子序列”已经存在;
    • 如果有序数组 tail 中存在大于 num 的元素,找到第 1 个,让它变小,这样我们就找到了一个“结尾更小”的“相同长度”的上升子序列。
public class Solution {

    public int lengthOfLIS(int[] nums) {
        int len = nums.length
        if (len <= 1){
            return len;
        }

        //定义tail:长度为 i + 1 的上升子序列的末尾最小是几
        int[] tail = new int[len];
        tail[0] = nums[0];

        //end表示tail最后一个已赋值索引
        int end = 0;
        for (int i = 1; i < len; i++) {
            if (nums[i] > tail[end]){
                end++;
                tail[end] = nums[i];
            } else {
                //用二分查找找到第一个>=nums[i]的元素
                int l = 0;
                int r = end;
                while (l < r){
                    int mid = l + (r - l) / 2;
                    if (tail[mid] < nums[i]){
                        l = mid + 1;
                    } else {
                      r = mid;
                    }
                }
                tail[l] = nums[i];
            }
        }
        end++;
        return end;
    }
}

646. 最长数对链(Medium)

给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。

现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。

给定一个对数集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

示例 :

输入: [[1,2], [2,3], [3,4]]
输出: 2
解释: 最长的数对链是 [1,2] -> [3,4]

注意:

给出数对的个数在 [1, 1000] 范围内。

解法一:

  • 这道题和上道题求最长上升子序列是一样的思想,但是这道题是不用保持原位置的,而上道题是要求数字之间的相对位置不能变。
  • 所以先对数组按照结束大小排序(跟排课的那道题目类似),接着进行暴力递归:每个数对都有两种选择,包括进或者不包括。
  • 不出意料,暴力递归解法超时了。
class Solution {
    public static int findLongestChain(int[][] pairs) {
        if (pairs == null || pairs.length == 0){
            return 0;
        }
        
        //对数组进行排序:按尾部大小进行排序
        Arrays.sort(pairs, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[1] - o2[1];
            }
        });
        return findLongestChain(pairs, Integer.MIN_VALUE, 0);
    }

    private static int findLongestChain(int[][] pairs, int pre, int cur) {
        if (cur == pairs.length){
            return 0;
        }

        //包含当前数对
        int taken = 0;
        if (pairs[cur][0] > pre){
            taken = 1 + findLongestChain(pairs, pairs[cur][1], cur + 1);
        }

        //不包括
        int nottaken = findLongestChain(pairs, pre, cur + 1);
        return Math.max(taken, nottaken);
    }
}

解法二:

  • 将暴力递归转为动态规划,避免暴力递归中的大量重复计算
  • 首先,先找变量:标记到第几个数组的变量cur和结果result,接着找转移方程:dp[i]代表前i(从0开始计数)个数形成的最长数对的状态转移方程:dp[i]=Math.max(dp[i-1],dp[j]+1),遍历到dp[i]时,遍历0~i-1,如果满足条件的就执行转移方程。
  • 转移方程就是从前一个推到后一个的方程,所以找的时候要关注这个推导过程
public int findLongestChain(int[][] pairs) {
    if (pairs == null || pairs.length == 0) {
        return 0;
    }

    //排序
    Arrays.sort(pairs, (a, b) -> (a[1] - b[1]));
    int n = pairs.length;
    int[] dp = new int[n];
    int res = 0;
    
    //初始化dp,最短是1
    Arrays.fill(dp, 1);
    
    //填充dp数组
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            //往前寻找的时候,如果发现满足条件的就比较 dp[i], dp[j] + 1 谁更大
            if (pairs[j][1] < pairs[i][0]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
        res = Math.max(res, dp[i]);
    }
    
    return res;
}

解法三:贪心,这道题和排课那道题思想是一致的,所以可以使用同样的方法解答:先对数组按照尾部进行排序,从第一个数对开始,依次判断后面的数对,能否与前面的数对组成数对链,能组的话长度就+1,这样找到的数对链就是最优解。

public int findLongestChain(int[][] pairs) {
    if (pairs == null || pairs.length == 0) {
            return 0;
        }

    //排序
    Arrays.sort(pairs, (a, b) -> (a[1] - b[1]));
    int n = pairs.length;
    int res = 1;
    int edn = pairs[0][1]
    
    for(int i=1;i<len;i++){
        if(pairs[i][0]>end){
            end=pairs[i][1];
            res++;
        }
    }
    return res;
}

376. 摆动序列(Medium)

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如,[1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3)是正负交替出现的。相反, [1,4,7,2,5]和[1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例 1:

输入: [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列。

示例 2:

输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。

示例 3:

输入: [1,2,3,4,5,6,7,8,9]
输出: 2

解法一:

  • 先分析题意:连续数字之间的差在正负之间交替,且数字相对位置不变的序列,成为摆动序列。
  • 每个数字有可能成为上升元素或者下降元素,这次找的是上升,下个就是下降,反之亦然
  • 最后比较两个大小,取大的返回
class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums.length < 2){
            return nums.length;
        }

        return 1 + Math.max(wiggleMaxLength(nums, true, 0), wiggleMaxLength(nums, false, 0));
    }

    private int wiggleMaxLength(int[] nums, boolean isUp, int cur) {

        //当前数作为摆动序列
        int maxCount = 0;
        for (int i = cur + 1; i < nums.length; i++) {
            if (isUp && nums[i] > nums[cur] || (!isUp && nums[i] < nums[cur])){
                maxCount = Math.max(maxCount, 1 + wiggleMaxLength(nums, !isUp, i));
            }
        }
        return maxCount;
    }
}

解法二:

  • 转为动态规划,因为一个元素可以既可以作为上升元素,也可以作为下降元素,所以数组一共有两种:dp[]和down[],up[i] 存的是目前为止最长的以第i个元素结尾的上升摆动序列的长度,down[i]也类似。
  • 变量是i和result,由于上升和下降是交替的,所以找到将第i个元素作为上升摆动序列的尾部的时候就更新 up[i]
  • 现在我们考虑如何更新 up[i],我们需要考虑前面所有的降序结尾摆动序列,也就是找到 down[j],满足 j < i 且 nums[i] >nums[j]。类似的, down[i]也会被更新。
public class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums.length < 2){
            return nums.length;
        }
            
        int[] up = new int[nums.length];
        int[] down = new int[nums.length];

        //遍历数组
        for (int i = 1; i < nums.length; i++) {
            for(int j = 0; j < i; j++) {
                //找到一个上升元素就与下降元素相比
                if (nums[i] > nums[j]) {
                    up[i] = Math.max(up[i],down[j] + 1);
                } else if (nums[i] < nums[j]) {
                    //反之同理
                    down[i] = Math.max(down[i],up[j] + 1);
                }
            }
        }
        
        return 1 + Math.max(down[nums.length - 1], up[nums.length - 1]);
    }
}

解法三:

线性动态规划,具体过程参考leetcode官方解答: 数组中的任何元素都对应下面三种可能状态中的一种:

  • 上升的位置,意味着 nums[i] > nums[i - 1]
  • 下降的位置,意味着 nums[i] < nums[i - 1]
  • 相同的位置,意味着 nums[i] == nums[i - 1]

更新的过程如下:

  • 如果 nums[i] > nums[i-1],意味着这里在摆动上升,前一个数字肯定处于下降的位置。所以 up[i] = down[i-1] + 1, down[i]与 down[i-1]保持相同。

  • 如果 nums[i] < nums[i-1],意味着这里在摆动下降,前一个数字肯定处于下降的位置。所以 down[i] = up[i-1] + 1, up[i]与 up[i-1]保持不变。

  • 如果 nums[i] == nums[i-1] ,意味着这个元素不会改变任何东西因为它没有摆动。所以 down[i]与 up[i] 与 down[i-1]和 up[i-1]都分别保持不变。

最后,我们可以将 up[length-1]和 down[length-1]中的较大值作为问题的答案,其中 lengthlength 是给定数组中的元素数目。

public class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums.length < 2){
            return nums.length;
        }
        
        int[] up = new int[nums.length];
        int[] down = new int[nums.length];
        up[0] = down[0] = 1;
        
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > nums[i - 1]) {
                up[i] = down[i - 1] + 1;
                down[i] = down[i - 1];
            } else if (nums[i] < nums[i - 1]) {
                down[i] = up[i - 1] + 1;
                up[i] = up[i - 1];
            } else {
                down[i] = down[i - 1];
                up[i] = up[i - 1];
            }
        }
        return Math.max(down[nums.length - 1], up[nums.length - 1]);
    }
}

解法四:贪心,具体解法和第三种线性动态规划一致,不同的是优化了空间,不再使用数组,而是用变量记录大小。

class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums == null || nums.length == 0)
            return 0;

        int up = 1;
        int down = 1;

        for (int i = 1; i < nums.length; i++) {
            //差值
            int c = nums[i] - nums[i - 1];
            //差值为正,就说明比down大一
            if (c > 0) {
                up = down + 1;
            } else if (c < 0) {
                //反之同理
                down = up + 1;
            }
        }
        return Math.max(up, down);
    }
}

最长公共子序列

对于两个子序列S1和S2,找出它们的最长公共子序列。(参考CyC2018大佬)

可以定义一个二维数组dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况:

  • 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1,即 dp[i][j] = dp[i-1][j-1] + 1。
  • 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,或者 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,取它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。 综上,最长公共子序列的状态转移方程为:

对于长度为 N 的序列 S1 和长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。

与最长递增子序列相比,最长公共子序列有以下不同点:

  • 针对的是两个序列,求它们的最长公共子序列。 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;
  • 在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j。
  • 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。

1143. 最长公共子序列(Medium)

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列。

一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 1:

输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。

示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。

示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。

提示:

1 <= text1.length <= 1000
1 <= text2.length <= 1000
输入的字符串只含有小写英文字符。

解法一:暴力递归,求两个字符串的最长公共字符串,分别以i和j开头递归这两个字符串,如果遇到S1.charAt(i)==S2.charAt(j)就++,否则i++之后的子序列长和j++之后的子序列长比较,取大的返回

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        if (text1 == null || text2 == null){
            return 0;
        }
        return longestCommonSubsequence(text1, text2, 0, 0);
    }

    private int longestCommonSubsequence(String text1, String text2, int i, int j) {
        //base case
        if (i == text1.length() || j == text2.length()){
            return 0;
        }

        //如果S1.charAt(i)==S2.charAt(j),长度加1
        if (text1.charAt(i) == text2.charAt(j)){
            return 1 + longestCommonSubsequence(text1, text2, i + 1, j + 1);
        }else {
            //如果不想等,取大的返回
            return Math.max(longestCommonSubsequence(text1, text2, i + 1, j), longestCommonSubsequence(text1, text2, i, j + 1));
        }
    }
}

解法二:改为动态规划,老套路。

  • 找变量:i和j以及res
  • 转移方程:dp[i][j]=dp[i-1][j-1]+1      如果S1.charAt(i)==S2.charAt(j)
  • dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]) 如果不等
public int longestCommonSubsequence(String text1, String text2) {
        char[] s1 = text1.toCharArray();
        char[] s2 = text2.toCharArray();
        int[][] dp = new int[s1.length + 1][s2.length + 1];
        
        for(int i = 1 ; i < s1.length + 1 ; i ++){
            for(int j = 1 ; j < s2.length + 1 ; j ++){
                //如果末端相同
                if(s1[i - 1] == s2[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[s1.length][s2.length];
    }