Medium | LeetCode 300. 最长递增子序列 | 动态规划 | 贪心 + 二分法
300. 最长递增子序列
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
示例 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
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104
进阶:
- 你可以设计时间复杂度为
O(n2)
的解决方案吗? - 你能将算法的时间复杂度降低到
O(n log(n))
吗?
解题思路
方法一: 动态规划
定义 dp[i] 为考虑前 i 个元素,以第 ii 个数字结尾的最长上升子序列的长度,注意 nums[i] 必须被选取。
我们从小到大计算 dp 数组的值,在计算 dp[i] 之前,我们已经计算出 dp[0…i−1] 的值,则状态转移方程为:
\[d p[i]=\max (d p[j])+1, \text { 其中 } 0 \leq j<i \text { 且 } \operatorname{num}[j]<\operatorname{num}[i]
\]
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) {
return 0;
}
// dp[i] 表示 前 i 个元素,以第 i 个数字结尾的最长上升子序列的长度
int[] dp = new int[nums.length];
dp[0] = 1;
int maxans = 1;
for (int i = 1; i < nums.length; i++) {
// 初始化 当前的长度为1
dp[i] = 1;
// 然后遍历前面的所有元素, 看是否能够接在后面, 同时更新最大长度
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
方法二: 贪心 + 二分查找
考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
基于上面的贪心思路,我们维护一个数组 d[i] ,表示长度为 ii 的最长上升子序列的末尾元素的最小值,用 len 记录目前最长上升子序列的长度,起始时len 为 1,d[1] = nums[0]。
同时我们可以注意到d[i] 是关于 ii 单调递增的。因为如果 d[j]≥d[i] 且 j<i,我们考虑从长度为 i 的最长上升子序列的末尾删除i−j 个元素,那么这个序列长度变为 j ,且第 j 个元素 x(末尾元素)必然小于 d[i],也就小于 d[j]。那么我们就找到了一个长度为 j 的最长上升子序列,并且末尾元素比 d[j] 小,从而产生了矛盾。因此数组 d 的单调性得证。
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 {
// 遇到更小的值, 就使用二分法找到d[i], 使d[i]刚好不大于当前值。
int l = 1, r = len;
// 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 pos 设为 0
int 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;
}