【Lintcode】76. Longest Increasing Subsequence

题目地址:

https://www.lintcode.com/problem/longest-increasing-subsequence/description

给定一个数组 n u m s nums ,求其(严格)最长上升子序列的长度。

法1:动态规划。设 f [ i ] f[i] 是以 n u m s [ i ] nums[i] 结尾的最长的上升子序列的长度,那么要计算 f [ i ] f[i] ,如果 n u m s [ i ] nums[i] 之前有数字 n u m s [ j ] nums[j] 小于 n u m s [ i ] nums[i] ,则 f [ i ] f[i] 可以更新为(如果更长的话) f [ j ] + 1 f[j]+1 ;如果没有数小于 n u m s [ i ] nums[i] ,则 f [ i ] = 1 f[i]=1 。以这个递推关系即可递推出所有以 n u m s [ i ] nums[i] 结尾的最长上升子序列的长度。最后只需要遍历 f f 取最大值即可。代码如下:

public class Solution {
    /**
     * @param nums: An integer array
     * @return: The length of LIS (longest increasing subsequence)
     */
    public int longestIncreasingSubsequence(int[] nums) {
        // write your code here
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int[] dp = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            dp[i] = Math.max(dp[i], 1);
        }
        
        int res = 0;
        for (int i : dp) {
            res = Math.max(res, i);
        }
        
        return res;
    }
}

时间复杂度 O ( n 2 ) O(n^2) ,空间 O ( n ) O(n)

法2:贪心法。我们想方设法来记录末尾数尽量小的上升子序列。遇到一个新数的时候,去找一下它能接在谁的后面。整个数组遍历完后,返回得到的最长的上升子序列的长度即可。严格证明附在后面,代码如下:

public class Solution {
    /**
     * @param nums: An integer array
     * @return: The length of LIS (longest increasing subsequence)
     */
    public int longestIncreasingSubsequence(int[] nums) {
        // write your code here
        if (nums == null || nums.length == 0) {
            return 0;
        }
        // f[i]是长度为i + 1的所有子序列中末尾数最小的那个子序列的末尾数
        int[] f = new int[nums.length];
        int cur = 0;
        f[0] = nums[0];
        
        for (int i = 1; i < nums.length; i++) {
            int last = findFirstLargerOrEqual(f, cur, nums[i]);
            if (last != -1) {
                f[last] = nums[i];
            } else if (nums[i] > f[cur]) {
                f[++cur] = nums[i];
            }
        }
        
        return cur + 1;
    }
    
    // 找在f[0,...,r]中找第一个大于或等于num的数的下标;不存在则返回-1
    private int findFirstLargerOrEqual(int[] f, int r, int num) {
        int l = 0;
        while (l < r) {
            int m = l + (r - l >> 1);
            if (f[m] < num) {
                l = m + 1;
            } else {
                r = m;
            }
        }
        
        if (f[l] > num) {
            return l;
        } else {
            return -1;
        }
    }
}

时间复杂度 O ( n log n ) O(n\log n) ,空间 O ( n ) O(n)

算法正确性证明:
首先证明 f [ i ] f[i] 确实存储了长度为 i + 1 i+1 的所有上升子序列中末尾数最小的那个子序列的末尾数,并且若 f [ i ] = 0 f[i]=0 ,说明遍历到当前数为止,未发现长度为 i + 1 i+1 的上升子序列。
为了证明这一点,先证 f f 是严格单调增的,如若不然,则存在 i i 使得 f [ i ] f [ i + 1 ] f[i]\ge f[i+1] ,那么存在以 f [ i + 1 ] f[i+1] 结尾的长度为 i + 2 i+2 的子序列,此子序列的第 i + 1 i+1 个数是比 f [ i ] f[i] 要小的,这与 f [ i ] f[i] 的定义矛盾了。所以 f f 单调增。
接下来使用数学归纳法。一开始遍历数组中第一个数, f [ 0 ] f[0] 初始化为数组第一个数,是没有问题的。假设后面某时刻 f [ 0 , . . . , i ] f[0,...,i] 都符合定义,接下来遍历到数组中的下一个数比如说是 x x ,如果 x > f [ i ] x>f[i] ,那么 x x 可以接在长度为 i + 1 i+1 且末尾数为 f [ i ] f[i] 的子序列后面,成为长度为 i + 2 i+2 的新的上升子序列,所以 f [ i + 1 ] f[i+1] 可以被更新为 x x ;否则,则要在 f [ 0 , . . . , i ] f[0,...,i] 中寻找第一个大于等于 x x 的数 f [ j ] f[j] ,并将 f [ j ] f[j] 更新为 x x ,理由是,由于 f f 单调上升,所以 f [ j 1 ] < x f[j-1]<x ,所以 x x 可以接到以 f [ j 1 ] f[j-1] 结尾的长度为 j j 的上升子序列后,这样就得到了一个长度为 j + 1 j+1 的上升子序列,所以 f [ j ] f[j] 可以更新为 x x (当然如果 j = 0 j=0 那结论更显然)。而对于 k < j k<j f [ k ] f[k] 都小于 x x ,所以无法得到更新;而对于 k > j k>j ,如果 f [ k ] f[k] 更新成为 x x ,则会导致可以构造出长度为 j + 1 j+1 且末尾数小于 x x 的上升子序列,与 f [ j ] f[j] 的定义矛盾。所以新遍历一个数后, f f 的定义仍然被保持。由数学归纳法,数组遍历完后, f f 非零数字的个数就是最长上升子序列的长度。

发布了354 篇原创文章 · 获赞 0 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_46105170/article/details/105004538