求解最长递增子序列(Longest Increasing Subsequence, LIS)问题的常见算法有动态规划和二分查找优化的动态规划。下面详细介绍这两种方法。
方法一:动态规划
- 定义一个数组
dp
,其中dp[i]
表示以nums[i]
结尾的最长递增子序列的长度。 - 初始化
dp
数组为 1,因为每个元素自身可以构成一个长度为 1 的递增子序列。 - 遍历序列,对于每个
nums[i]
,检查nums[0]
到nums[i-1]
中的每个元素nums[j]
,如果nums[j]
小于nums[i]
,则更新dp[i]
。 - 最终答案为
dp
数组中的最大值。
public class Solution {
/**
* 求最长递增子序列长度(动态规划)
*/
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
int[] dp = new int[n];
int maxLength = 1;
for (int i = 0; i < n; i++) {
dp[i] = 1; // 每个元素自身可以构成一个长度为1的递增子序列
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxLength = Math.max(maxLength, dp[i]);
}
return maxLength;
}
}
时间复杂度为 O ( n 2 ) O(n^2) O(n2),详细解释可以参考:
方法二:动态规划 + 二分查找
- 定义一个数组
dp
,其中dp[i]
表示长度为i+1
的递增子序列的最后一个元素的最小值(这里dp
数组的定义和方法一不同,注意区分)。 - 遍历序列,对于每个
nums[i]
,使用二分查找在dp
中找到第一个大于等于nums[i]
的元素,将其替换为nums[i]
,如果没有找到,则将nums[i]
添加到dp
的末尾。 - 最终
dp
的长度即为最长递增子序列的长度。
public class Solution {
/**
* 求最长递增子序列长度(动态规划+二分查找)
*/
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
int size = 0;
for (int num : nums) {
int left = 0, right = size;
// 二分查找位置
while (left < right) {
int mid = left + (right - left) / 2;
if (dp[mid] < num) {
left = mid + 1;
} else {
right = mid;
}
}
// 更新dp数组
dp[left] = num;
// 如果num添加到了dp的末尾,增加 size
if (left == size) {
size++;
}
}
return size;
}
}
时间复杂度为 O ( n log n ) O(n \log n) O(nlogn) ,其中:
- 二分查找的作用:保持
dp
数组的有序性;提高查找和更新的效率。 - 更新
dp
数组:如果num
可以扩展当前最长递增子序列(即num
大于dp
的所有元素),则将num
添加到dp
的末尾;否则,用num
替换dp
中第一个大于或等于num
的元素,以保证dp
中每个位置的元素是递增的最小值。
扩展:求最长递增子序列的具体序列结果
只需要记录一下 nums[i]
在长度为 i+1
的递增子序列中的位置,最后逆推便可得到LIS具体序列结果。
class Solution {
/**
* 求最长递增子序列的具体结果
* (动态规划+二分查找)
*/
public List<Integer> resultOfLIS(int[] nums) {
int[] dp = new int[nums.length]; // 记录长度为 i+1 的递增子序列的最小末尾元素
int[] pos = new int[nums.length]; // 记录 nums[i] 在长度为 i+1 的递增子序列中的位置
int size = 0;
for (int i = 0; i < nums.length; i++) {
int left = 0, right = size;
// 二分查找位置
while (left < right) {
int mid = left + (right - left) / 2;
if (dp[mid] < nums[i]) {
left = mid + 1;
} else {
right = mid;
}
}
// 更新 dp 数组
dp[left] = nums[i];
// 更新 pos 数组
pos[i] = left;
// 如果 num 添加到了 dp 的末尾,增加 size
if (left == size) {
size++;
}
}
// 逆推出最长递增子序列
List<Integer> result = new LinkedList<>();
for (int i = nums.length - 1; i >= 0; i--) {
if (pos[i] == size - 1) {
result.add(0, nums[i]);
size--;
}
}
return result;
}
}