首页 > 技术文章 > 动态规划学习之LeetCode最长递增子序列(第300、646、376题)

yxym2016 2020-04-06 15:21 原文

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

示例:

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

分析: 需要注意的是本题要求的的子序列的长度,而只要保证在原数组中的相对顺序不变,可以任意选取数组中的元素组成一个序列,而递增子序列的含义是:假设原始序列是 {S1, S2,...,Sn},如果在子序列 {Si1, Si2,..., Sim}( i1、i2 ... im 保持递增,im<=n)中,当下标ix > iy时,Six > Siy,称子序列为原序列的一个递增子序列 。所以还是使用动态规划的方法,具体过程如下:

  1.定义一个数组dp,dp[i]的含义是以原序列中第i个字符结尾的序列的最长递增子序列的长度;

  2.寻找关系式,还是采用内外循环的方式,内循环用于寻找以当前访问的字符结尾的序列的最长递增子序列的长度,即当前数如果大于在它之前的某个数,那就将它之前的那个数的最长递增子序列的长度加1然后与当前的最大值进行比较。

  3.初始条件就是dp[0] = 1;

实现代码:

    public int lengthOfLIS(int[] nums) {
        if (nums.length <= 1)
            return nums.length;
        int[] dp = new int[nums.length];
        int n = nums.length;
        dp[0] = 1;
        int res = 1;

        for (int i = 1; i < n; i++) {
            int max = 1;
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j])
                    max = Math.max(dp[j]+1,max);
            }
            dp[i] = max;
            if (dp[i]>res)
                res = dp[i];
        }
        return res;
    }

上面的时间复杂度为O(n*n),下面采用优化改进的方法,降低时间复杂度,即借助额外的空间,定义一个数组tails,用于存储截止到当前访问的元素为止的序列的最长递增子序列,其中也要通过二分查找去确定当前访问的元素应该是否放入tails数组以及如果放入应该放在哪个位置,这些位置可能包括,替换数组中间某些较大的元素,或者增加到数组末尾。具体的代码实现如下:

    public int lengthOfLIS_better(int[] nums) {
        if (nums.length <= 1)
            return nums.length;
        int[] tails = new int[nums.length];
        int len = 0;
        for (int num : nums) {
            int index = binarySearch(tails,len,num);
            tails[index] = num;
            if (index == len)
                len++;
        }
        return len;
    }
    private int binarySearch(int[] tails,int len,int key){
        int left = 0,right = len;
        int mid = left + (right - left) / 2;
        while (left < right){
            if (tails[mid] == key)
                return mid;
            else if (tails[mid] > key){
                /*
                此处为何是 right = mid 而不是 right = mid-1?
                因为我们要找的是这个数组中与key相等的值的位置,
                在整个数组中没有与key想等的值的时候,
                应该要找的是key值应该插入到数组中的位置,而同时这个数组是有序的,所以接下来我们
                在寻找的时候,要插入的位置肯定不是tails[mid] < key的元素位置,
                而一定是tails[mid] > key 的元素的位置
                 */
                right = mid;
            }else {
                left = mid + 1;
            }
            mid = left + (right - left) / 2;
        }
        return left;
    }

以上代码的时间复杂度是O(n * logn)

 

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

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

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

示例:

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

分析:注意题目的信息,题中说可以以任何顺序选择其中的一些数对来构造,也就是为了处理方便,我们有必要对这个二位数组以一维数组为单位进行排序,由于每一个数对中,第一个数字总是比第二个数字小,所以就以数对的第一个数字为排序基准进行升序排序。然后采用的思路依然是动态规划的思想,采用双层遍历,外层用于访问每一个数对元素,内层用于确定以当前数对元素结尾的数对序列中,最长的链对是多少。具体过程:

  1.定义一个数组dp,dp[i]表示以当前访问的第i个数对结尾的数对序列含有的最长链对的长度是多少;

  2.寻找数组之间的关系,我们要寻找的是对于当前访问的数对元素,它的前面的所有数对元素中,哪个和当前数对元素既符合数对链的关系,又是组成数对链的长度最长的,最后在从第0个到第i-1个遍历完之后,最大值赋予dp[i]

  3.初始值,dp[0] = 1;

实现的代码如下:

    public int findLongestChain(int[][] pairs) {

        int m = pairs.length;
        int[] dp = new int[m];
        int res = 0;
        dp[0] = 1;
        // 这里使用了lambda表达式,(a, b) -> (a[0] - b[0])表示二维数组中的元素(一维数组)以其第一个元素值为比较值进行升序排序,
        // 若为(a[0] - b[0]),则是降序排序
        Arrays.sort(pairs, (a, b) -> (a[0] - b[0]));
        for (int i = 1; i < m; i++) {
            int max = 1;
            for (int j = 0; j < i; j++) {
                if (pairs[i][0] > pairs[j][1])
                    max = Math.max(dp[j]+1,max);
            }
            dp[i] = max;
            if (max > res)
                res = max;
        }
        return res;
    }

 

题目:如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例:

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

分析:找出题目的要点,数字之差严格的在正负之间交替,也即是数字大小变化是波浪形的,没有相等的,也不能是连续递增或者连续递减的,题目要求的是序列,只要保证元素的相对顺序即可。

  1.定义两个状态转移数组up、down,up[i]表示以当前访问的第i个元素结尾的最长摆动上升序列的长度,down[i]表示以当前访问的第i个元素结尾的最长摆动下降序列的长度;

  2.寻找关系,对于当前访问的元素nums[i],如果它大于前一个元素nums[i-1],那么前面一个数字必须处于摆动下降的位置;对于当前访问的元素nums[i],如果它小于前一个元素nums[i-1],那么前面一个数字必须处于摆动上升的位置;如果两个元素相等,那么就保持up和down的值与前面的值不变。也就是说,只有当连续数字之间的差在正负之间要个交替的时候,down和up的值才会交替连续发生变化,否则就会一直停滞在某个摆动下降或者摆动上升的位置。

  3.初始值,因为少于两个元素的序列也是摆动序列,所以down[0] = 1,up[0] = 1。

实现代码:

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

优化一下,不再使用两个额外的数组空间,而使用两个变量。

推荐阅读