300. 最长递增子序列
300. 最长递增子序列
题目来源
题目分析
给定一个整数数组
nums
,要求找到其中最长严格递增子序列的长度。严格递增子序列指的是对于子序列中的每一对相邻元素nums[i]
和nums[j]
,均满足i < j
且nums[i] < nums[j]
。
题目难度
- 难度:中等
题目标签
- 标签:动态规划
题目限制
1 <= nums.length <= 2500
-10^4 <= nums[i] <= 10^4
进阶
你能否设计出时间复杂度为 O(n^2)
或更优的解决方案?
解题思路
思路1:动态规划
-
问题定义:设
dp[i]
表示以nums[i]
结尾的最长递增子序列的长度。 -
状态转移:
- 对于每个
i
,遍历从0
到i-1
的所有元素nums[j]
,如果nums[i] > nums[j]
,则dp[i]
可以更新为dp[j] + 1
。 - 即
dp[i] = max(dp[j]) + 1
,其中j < i
且nums[j] < nums[i]
。
- 对于每个
-
初始化:
dp
数组初始值为1
,因为每个元素单独作为一个子序列时,长度为1
。
-
最终结果:
- 最后返回
dp
数组中的最大值,即为最长递增子序列的长度。
- 最后返回
思路2:贪心 + 二分查找
-
核心思想:
- 通过维护一个数组
g
,用以保存当前能够形成的最长递增子序列的最小结尾元素,g[i]
表示长度为i+1
的子序列的最小末尾元素。
- 通过维护一个数组
-
算法步骤:
- 遍历
nums
数组中的每个元素nums[i]
,在数组g
中找到第一个大于等于nums[i]
的元素并替换之。如果nums[i]
比g
中所有元素都大,则将其追加到g
的末尾。
- 遍历
-
优化:
- 二分查找可以有效地找到
g
中需要替换的位置,从而将时间复杂度优化为O(n log n)
。
- 二分查找可以有效地找到
核心算法步骤
-
动态规划法:
- 初始化
dp
数组为全1
。 - 通过两重循环更新
dp
数组。 - 返回
dp
数组中的最大值。
- 初始化
-
贪心 + 二分查找法:
- 使用二分查找找到需要更新的
g
数组位置。 - 更新或追加
g
数组。 - 返回
g
数组的长度。
- 使用二分查找找到需要更新的
代码实现
以下是求解最长递增子序列问题的 Java 代码:
方法1:动态规划
/**
* 300. 最长递增子序列
* @param nums 整数数组
* @return 最长递增子序列的长度
*/
public int lengthOfLIS(int[] nums) {
int ans = 0;
int n = nums.length;
int[] dp = new int[n];
for (int i = 0; i < n; i++) {
dp[i] = 1;
}
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
for (int i = 0; i < n; i++) {
ans = Math.max(ans, dp[i]);
}
return ans;
}
方法2:贪心 + 二分查找
/**
* 300. 最长递增子序列
* @param nums 整数数组
* @return 最长递增子序列的长度
*/
public int lengthOfLIS2(int[] nums) {
int n = nums.length;
int[] g = new int[n];
g[0] = nums[0];
int len = 1;
for (int i = 1; i < n; i++) {
int l = 0, r = len - 1;
while (l <= r) { // [l,r)
int mid = l + r >>> 1;
if (g[mid] < nums[i]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
g[l] = nums[i];
if (l == len) {
len++;
}
}
return len;
}
代码解读
-
lengthOfLIS
方法:- 通过双层循环,遍历所有
nums[i]
之前的元素nums[j]
,找到最长递增子序列并更新dp
。
- 通过双层循环,遍历所有
-
lengthOfLIS2
方法:- 通过二分查找来确定
g
数组中第一个大于等于nums[i]
的位置,并进行更新或追加。
- 通过二分查找来确定
性能分析
-
动态规划法:
- 时间复杂度:
O(n^2)
,其中n
是数组nums
的长度。 - 空间复杂度:
O(n)
,因为我们使用了dp
数组。
- 时间复杂度:
-
贪心 + 二分查找法:
- 时间复杂度:
O(n log n)
,因为二分查找的引入将时间复杂度优化为O(log n)
。 - 空间复杂度:
O(n)
,因为我们使用了g
数组。
- 时间复杂度:
测试用例
你可以使用以下测试用例来验证代码的正确性:
int[] nums1 = {10,9,2,5,3,7,101,18};
int result1 = lengthOfLIS(nums1);
System.out.println(result1); // 输出: 4
int[] nums2 = {0,1,0,3,2,3};
int result2 = lengthOfLIS(nums2);
System.out.println(result2); // 输出: 4
int[] nums3 = {7,7,7,7,7,7,7};
int result3 = lengthOfLIS(nums3);
System.out.println(result3); // 输出: 1
扩展讨论
其他实现
- 递归 + 记忆化:
- 可以通过递归加记忆化的方法来解决该问题,但可能在时间复杂度上没有二分查找法优化得好。
总结
最长递增子序列问题通过动态规划和贪心 + 二分查找两种方法来解决。动态规划法简单直观,但时间复杂度较高,而贪心 + 二分查找法在时间复杂度上具有优势,适用于数据规模较大的情况。