最长递增子序列(LIS)

给定一乱序整型数组,求其最长递增子序列。
例如:

Input: [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4

注意:
* 最长递增子序列可能不止一个,只需返回其长度即可。
* 算法复杂度应该小于O(n^2)

提升:你能将时间复杂度提升至O(nlogn)吗?

求解

方法 1 暴力搜索(超出时间限制)

算法
最简单的方法是尝试寻找所有递增子序列,然后返回递增子序列的最大长度。为此,我们使用递归函数lengthofLIS,该函数返回从当前元素(curpos)向前(包括当前元素)的LIS长度。每次函数调用时,考虑两种情况:
1. 当前元素比LIS中的前一个元素大。这种情况下,我们可以将当前元素纳入LIS。然后求出包含当前元素后的LIS长度。同时,我们也求出不包含当前元素的LIS长度。最后返回两个长度的最大值。
2. 当前元素小于LIS中的前一个元素。这种情况下,不能将当前元素放入LIS中。因此,我们返回不包含当前元素的LIS长度。

public class Solution {

    public int lengthOfLIS(int[] nums) {
        return lengthofLIS(nums, Integer.MIN_VALUE, 0);
    }

    public int lengthofLIS(int[] nums, int prev, int curpos) {
        if (curpos == nums.length) {
            return 0;
        }
        int taken = 0;
        if (nums[curpos] > prev) {
            taken = 1 + lengthofLIS(nums, nums[curpos], curpos + 1);
        }
        int nottaken = lengthofLIS(nums, prev, curpos + 1);
        return Math.max(taken, nottaken);
    }
}

复杂度分析
* 时间复杂度:O(2^n)。递归数大小2^n。
* 空间复杂度:O(n^2)。使用了n*n的数组内存。

方法 2 记忆化递归(超出空间限制)

算法
在上一个方法中,许多参数相同的递归过程被多次调用。通过将递归调用结果保存在一个二维记忆数组memo中,可以消除冗余的调用。memo[i][j]表示num[i]作为前一个元素放入/不放入LIS,并且num[j]作为当前元素放入/不放入LIS时的最长LIS。(memo[i][j] represents the length of the LIS possible using nums[i]nums[i] as the previous element considered to be included/not included in the LIS, with nums[j]nums[j] as the current element considered to be included/not included in the LIS.)。num表示所给定的数组。

ps:由暴力递归改写。递归函数可变参数 preIndex、curpos,无后效性,故memo[i][j]中缓存由特定递归过程(preIndex, curpos)计算出的值。考虑到元素的取值范围无界,在上一个方法中使用的pre参数(表示LIS中的上一个元素的值)需要替换,这里使用preIndex表示该元素在nums数组中的位置。

public class Solution {
    public int lengthOfLIS(int[] nums) {

        // preIndex的取值范围[-1, nums.length - 1],-1表示无前缀。故长度为nums.length + 1,为了将下标为-1的值放入数组,元素整体向后移一位。
        int memo[][] = new int[nums.length + 1][nums.length];

        for (int[] l : memo) {
            Arrays.fill(l, -1);
        }
        return lengthofLIS(nums, -1, 0, memo);
    }
    public int lengthofLIS(int[] nums, int previndex, int curpos, int[][] memo) {

        // base case
        if (curpos == nums.length) {
            return 0;
        }

        // serach first
        if (memo[previndex + 1][curpos] >= 0) {
            return memo[previndex + 1][curpos];
        }

        // compute and cache
        int taken = 0;
        if (previndex < 0 || nums[curpos] > nums[previndex]) {
            taken = 1 + lengthofLIS(nums, curpos, curpos + 1, memo);
        }
        int nottaken = lengthofLIS(nums, previndex, curpos + 1, memo);
        memo[previndex + 1][curpos] = Math.max(taken, nottaken);

        return memo[previndex + 1][curpos];
    }
}

复杂度分析
* 时间复杂度:O(n^2),递归树的大小为n^2。
* 空间复杂度:O(n^2),memo数组的吊销为n*n。

方法 3 动态规划(通过)

算法
动态规划方法依赖于这样一个事实——以i元素结尾的最长子序列不依赖于其后续元素。因此,如果已知以i元素结尾的LIS长度,可以求出以i+1元素结尾的LIS长度(通过向前搜索,尝试将i+1元素追加至其后)。
创建数组dp来存储相关数据。dp[i]表示以索引i元素作为结尾的子序列的最大长度。为了求解dp[i],尝试将当前元素(nums[i])追加到每一个既有的递增子序列中,使得添加完当前元素后的新序列仍然是递增序列,若无法添加至仍以既有子序列,则以i元素结尾的LIS长度为1。dp[i]的表达式为:

dp[i] = max(dp[j]) + 1, 0 <= j < i and nums[i] > nums[j]

即,对于dp[i],求nums[i]结尾的最长递增子序列,在nums[0..i-1]中选出比nums[i]小且长度最长(dp[j] max)的。

最后,dp[i]的最大值即为所求答案,

LIS = max(dp[i]), 0 <= i < n

ps:原著英文较差,此处意译。

public 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 < dp.length; i++) {
            int maxval = 0;
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    maxval = Math.max(maxval, dp[j]);
                }
            }
            dp[i] = maxval + 1;
            maxans = Math.max(maxans, dp[i]);
        }
        return maxans;
    }
}

复杂度分析
* 时间复杂度:O(n^2),两个for循环。
* 空间复杂度:O(n)。

方法 4 动态规划 + 二分查找(通过)

算法
方法3花费了大量时间寻找最大dp[j]上(第二个for循环),如果dp[]是一个递增数列,那么我们可以使用二分查找进行优化,使得整个算法的复杂度下降到O(nlogn)。方法3中dp[i]保存了以nums[i]元素结尾的LIS长度,这里我们使用dp[i]保存所有长度为i+1的递增子序列中末尾元素的最小值。根据这个最小值,可以判断num数组中的后续元素是否可以追加到既有IS中以形成更长的IS。由于dp数组是递增的,所以可以使用二分查找。
举例:

nums: 2 1 5 3 6 4 8 9 7

dp[]: 2………// 遍历至nums[0],当前长度为1的子序列末尾的最小值为2.

dp[]: 1………// 遍历至nums[1],nums[1] < dp[0],故不能追加至dp[0]的尾部以形成更大的子序列,只能单独形成长度为1的子序列。因为1 < 2,更新dp[0]

dp[]: 1 5…….// nums[2],nums[2] < dp[0],可与dp[0]形成长度为2的子序列,故dp[1]=5,表示在所有长度为2的子序列中,末尾元素最小的为5.

dp[]: 1 3…….// dp[0] < num[3] < dp[1],更新dp[1],表示在长度为2的子序列中,1 3 比 1 5拥有更小的末尾元素。

dp[]: 1 3 6…..// num[4] > dp[2],可形成长度为3的子序列。

dp[]: 1 3 4…..// dp[1] < num[5] < dp[2],更新dp[2],形成末尾元素更小的3长子序列。

dp[]: 1 3 4 8…// num[6] > dp[2],形成更长的子序列。

dp[]: 1 3 4 8 9.// 形成更长的子序列

dp[]: 1 3 4 7 9.// dp[2] < num[8] < dp[3],更新dp[3],表示若形成长度为4的子序列,1 3 4 7较 1 3 4 8拥有更小的末尾元素。但不代表 1 3 4 7 9形成了长度为5的子序列,实际上长度为5的子序列为 1 3 4 8 9。dp数组并不是保存最终的最长子序列,只是保存所有指定长度递增子序列中末尾元素的最小值。dp[3]看似无需更新,但若num后续出现8 9,最终的LIS将会是6.

最终,dp.size = 5,表示LIS=5,且所有该长度序列中最小以9结尾。

public class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        int len = 0;
        for (int num : nums) {
            int i = Arrays.binarySearch(dp, 0, len, num);
            if (i < 0) {
                i = -(i + 1);
            }
            dp[i] = num;
            if (i == len) {
                len++;
            }
        }
        return len;
    }
}

复杂度分析
* 时间复杂度:O(nlogn),二叉搜索花费logn,被调用了n次。
* 空间复杂度:O(n)

参考:
https://leetcode.com/problems/longest-increasing-subsequence/solution/

你可能感兴趣的:(最长递增子序列(LIS))