LeedCode300 最长上升子序列问题 动态规划 和 二分查找+贪心算法的区别

3.5最长上升子序列

3.5.1题目描述

LeedCode题目:300.最长上升子序列
描述:
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是4。

3.5.2思路与算法

首先要明确这个题目只需要求上升子序列的个数,不用具体求出是哪几个序列数字,并且不是每个数字逐个严格的上升!可以隔着上升,比如2,5,3,7的上升序列,2 3 7中间可以隔一个5。
定义 dp[i]为考虑前 i 个元素,以第 i个数字结尾的最长上升子序列的长度,注意nums[i] 必须被选取。我们从小到大计算dp[] 数组的值,在计算dp[i] 之前,我们已经计算出 dp[0 …i−1] 的值,则状态转移方程为:

dp[i] = max(dp[j])+1其中0<j<i且num[j]<num[i]

即考虑往dp[0…i−1] 中最长的上升子序列后面再加一个nums[i]。由于 dp[j] 代表nums[0…j] 中以nums[j] 结尾的最长上升子序列,所以如果能从 dp[j] 这个状态转移过来,那么nums[i] 必然要大于nums[j],才能将nums[i] 放在 nums[j] 后面以形成更长的上升子序列。
最后,整个数组的最长上升子序列即所有dp[i] 中的最大值。

max(dp[i]),其中0≤i<n

3.5.3 复杂度分析

时间复杂度:O(n2),其中n为数组nums 的长度。动态规划的状态数为 n,计算状态dp[i]时,需要 O(n)的时间遍历dp[0…i−1] 的所有状态,所以总时间复杂度为 O(n2)。

空间复杂度:O(n),需要额外使用长度为n的dp 数组。

3.5.4 代码

class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }
        int[] dp = new int[nums.length];
        dp[0] = 1;
        int maxans = 1;
        for (int i = 1; i < nums.length; i++) {
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {//注意这里是两个nums[i],nums[j]的比较
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            maxans = Math.max(maxans, dp[i]);
        }
        return maxans;
    }
}

3.5.4 贪心 + 二分查找

前言:

其实解法二,不用看证明想的太复杂,就是以输入数据为基础构建一个递增的d【】数组,d【】数组让最后一个数字尽可能小(可以证明这个数组的单调性),d数组整体是保持上升的一个序列,然后向后遍历输入数组nums【】的时候用一个二分查找插入到d【】数组里,最后只要看下d数组多长(就是len多大),就是最大上升子序列了。对比一下代码
if (d[mid] < nums[i]) 你就知道这是基于二分查找的插入排序了

贪心 + 二分查找:
利用无序列表降低时间复杂度,考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
基于上面的贪心思路,我们维护一个数组d[i] ,表示长度为 i 的最长上升子序列的末尾元素的最小值(详细解释见下),用len记录目前最长上升子序列的长度,起始时 len为 1,d[1]=nums[0]。

无序列表最关键的一句在于:数组 d[i]表示长度为 i 的最长上升子序列的末尾元素的最小值,即在数组
1,2,3,4,5,6中长度为3的上升子序列可以为1,2,3也可以为2,3,4等等但是d[3]=3,即子序列末尾元素最小为3。

这个证明可以不管!就是证明最后d[]有几个元素就有几个上升序列

无序列表解释清了数组d的含义之后,我们接着需要证明数组d具有单调性,即证明i<j时,d[i]<d[j],使用反证法,假设存在k<j时,d[k]>d[j],但在长度为j,末尾元素为d[j]的子序列A中,将后j-i个元素减掉,可以得到一个长度为i的子序列B,其末尾元素t1必然小于d[j](因为在子序列A中,t1的位置上在d[j]的后面),而我们假设数组d必须符合表示长度为 i 的最长上升子序列的末尾元素的最小值,此时长度为i的子序列的末尾元素t1<d[j]<d[k],即t1<d[k],所以d[k]不是最小的,与题设相矛盾,因此可以证明其单调性
无序列表证明单调性有两个好处:1.可以使用二分法;2.数组d的长度即为最长子序列的长度;

以输入序列[0,8,4,12,2] 为例:
第一步插入 0,d=[0];
第二步插入 8,d=[0,8];
第三步插入 4,d=[0,4];
第四步插入 12, d=[0,4,12];
第五步插入 2,d=[0,2,12]。
最终得到最大递增子序列长度为 33。
时间复杂度: O(nlogn)
空间复杂度:O(n)

代码

class Solution {
    public int lengthOfLIS(int[] nums) {
        int len = 1, n = nums.length;
        if (n == 0) {
            return 0;
        }
        int[] d = new int[n + 1];
        d[len] = nums[0];
        for (int i = 1; i < n; ++i) {
            if (nums[i] > d[len]) {
                d[++len] = nums[i];
            } else {
                int l = 1, r = len, pos = 0; // 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 pos 设为 0
                while (l <= r) {
                    int mid = (l + r) >> 1;
                    if (d[mid] < nums[i]) {
                        pos = mid;
                        l = mid + 1;
                    } else {
                        r = mid - 1;
                    }
                }
                d[pos + 1] = nums[i];
            }
        }
        return len;
    }
}

对比方法1和方法2的if语句判定条件,就能想明白了

https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-by-leetcode-soluti/
转自力扣官方和大佬评论,加上自己的一些理解。

©️2020 CSDN 皮肤主题: 游动-白 设计师:上身试试 返回首页