算法-动态规划(最长递增子序列LIS)

184 阅读3分钟

算法-动态规划

最长递增子序列LIS(longest increasing subsequence)

最长递增子序列(LIS)是指在一个序列中找到一个子序列,其元素的数值严格递增,且长度尽可能长。这个子序列中的元素在原序列中不需要是连续的,但相对顺序必须保持一致。例如,在数组 [10,9,2,5,3,7,101,18]中,最长递增子序列是[2,3,7,101],其长度为 4

注: 子序列可以不用连续,可以从原数组中任意选择组成,但是必须保证前后一致性

示例 1:

输入: nums = [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入: nums = [0,1,0,3,2,3]
输出: 4

示例 3:

输入: nums = [7,7,7,7,7,7,7]
输出: 1

这道题目基本没有什么暴力解法或者朴素解法,所以使用动态规划(我也是看大佬分析的^_^)

动态规划最重要的步骤,推导递推公式,也就是将问题拆分为子问题,然后逐步求解

我们定义一个dp[] 动态数组dp[i]表示数据中第i个下标之前最大可以求得最长递增子序列,只要dp[i] 大于dp[i-1]... dp[0]的值,dp[i]= dp[i-k] + 1, 每一个i下标的值,都是由前面所有dp的计算最大值max,然后继续推导dp[i+1] 如下图,我们可以看到7的值来自前面的2,5,3 再次基础上都是+1,最后取值就看最大值就行了,符合最长递增子序列的逻辑

image.png

解法一 动态规划

java实现代码如下 时间复杂度O(N²)

class Solution {
    public static int lengthOfLIS(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        int ans = 0;
        for (int i = 0; i < n; i++) {
            int max = 1;
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    max = Math.max(max, dp[j] + 1);
                }
            }
            dp[i] = max;
            ans = Math.max(ans,max);
        }
        return ans;
    }
}

解法二 贪心 + 二分查找

这种解法十分巧妙,非常不好想到,也不直观理解 需要用到二分查找的方法实现降低复杂度 解题思路: 降低复杂度切入点: 解法一中,遍历计算 dp 列表需 O(N),计算每个 dp[k] 需 O(N)。

新的状态定义:

我们考虑维护一个列表 tails,其中每个元素 tails[k] 的值代表 长度为 k+1 的子序列尾部元素的值。 如 [1,4,6] 序列,长度为 1,2,3 的子序列尾部元素值分别为 tails=[1,4,6]。 状态转移设计:

设常量数字 N,和随机数字 x,我们可以容易推出:当 N 越小时,N<x 的几率越大。例如: N=0 肯定比 N=1000 更可能满足 N<x。

在遍历计算每个 tails[k],不断更新长度为 [1,k] 的子序列尾部元素值,始终保持每个尾部元素值最小 (例如 [1,5,3]], 遍历到元素 5 时,长度为 2 的子序列尾部元素值为 5;当遍历到元素 3 时,尾部元素值应更新至 3,因为 3 遇到比它大的数字的几率更大)。

tails 列表一定是严格递增的: 即当尽可能使每个子序列尾部元素值最小的前提下,子序列越长,其序列尾部元素值一定更大。

时间复杂度:O(nlogn)

class Solution {
    public int lengthOfLIS(int[] nums) {
        List<Integer> g = new ArrayList<>();
        for (int x : nums) {
            int j = lowerBound(g, x);
            if (j == g.size()) {
                g.add(x); // >=x 的 g[j] 不存在
            } else {
                g.set(j, x);
            }
        }
        return g.size();
    }

    // 开区间写法
    private int lowerBound(List<Integer> g, int target) {
        int left = -1, right = g.size(); // 开区间 (left, right)
        while (left + 1 < right) { // 区间不为空
            // 循环不变量:
            // nums[left] < target
            // nums[right] >= target
            int mid = left + (right - left) / 2;
            if (g.get(mid) < target) {
                left = mid; // 范围缩小到 (mid, right)
            } else {
                right = mid; // 范围缩小到 (left, mid)
            }
        }
        return right; // 或者 left+1
    }
}

本文参考

作者:灵茶山艾府

链接:leetcode.cn/problems/lo…