算法-动态规划
最长递增子序列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,最后取值就看最大值就行了,符合最长递增子序列的逻辑
解法一 动态规划
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
}
}
本文参考
作者:灵茶山艾府