文章目录
最长上升( 递增 )子序列与最长连续上升( 递增 )子序列( LIS )
1. 最长上升子序列
LintCode题目地址: 76. 最长上升子序列
LeetCode题目地址: 300. 最长上升子序列
题目描述
给定一个整数序列,找到最长上升子序列( LIS ),返回 LIS 的长度。
说明
最长上升子序列的定义:
最长上升子序列问题是在一个无序的给定序列中找到一个尽可能长的由低到高排列的子序列,这种子序列不一定是连续的或者唯一的。
https://en.wikipedia.org/wiki/Longest_increasing_subsequence
样例
样例 1:
输入: [5,4,1,2,3]
输出: 3
解释:
LIS 是 [1,2,3]
样例 2:
输入: [4,2,4,5,3,7]
输出: 4
解释:
LIS 是 [2,4,5,7]
暴力 dp 解法
假设在长度为 n
的数组 A
中,以 A[j]
结尾的数组序列的最长上升子序列长度为 L[j]
,则
L ( j ) = { m a x L ( i ) + 1 , i < j 并且 A [ i ] < A [ j ] } L(j) = \{ maxL(i) + 1, i<j \text{并且} A[i]<A[j] \} L(j)={maxL(i)+1,i<j并且A[i]<A[j]}
也就是说,我们需要遍历在 j
之前的所有位置 i
(从 0
到 j-1
),找出满足条件 A[i] < A[j]
的 L(i)
,求出 max(L(i)) + 1
即为 L(j)
的值。最后遍历所有的 L[j]
(从 0
到 n-1
),找出最大值即为最大上升子序列的长度.
public int longestIncreasingSubsequence(int[] nums) {
if (nums == null || nums.length == 0)
return 0;
int res = 0;
int n = nums.length;
int[] dp = new int[n];
for (int j = 0; j < n; ++j) {
dp[j] = 1;
for (int i = 0; i < j; ++i) {
if (nums[i] < nums[j]) {
dp[j] = Math.max(dp[j],1 + dp[i]);
}
}
res = Math.max(res, dp[j]);
}
return res;
}
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n ) O(n) O(n)
转化为LCS求解
原数组:A:{5, 6, 7, 1, 2, 8}
排序后:B: {1, 2, 5, 6, 7, 8}
原数组的子序列顺序保持不变,而排序后的数组本身就是递增的,这样就保证了两个序列的最长公共子序列就是递增的
A
和 B
的最长公共子序列就是原数组的最长上升子序列
时间复杂度: O ( n 2 ) O (n^2) O(n2)
排序 O ( l o g n ) O (logn) O(logn),LCS 求解时间复杂度 O ( n 2 ) O (n^2) O(n2)
空间复杂度: O ( n ) O (n) O(n)
二分查找+贪心
参考博客:
对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。因此维护一个 B[]
数组,len
表示当前最长的 LIS
长度,如果 nums[i] > B[len-1]
,就把 nums [i]
添加到当前最长的 LIS
序列后面,即 B[len] = nums[i]
否则,就用 nums [i]
去更新 B[]
数组
例如,有以下序列 A[] = {3, 1, 2, 6, 4, 5, 10, 7}
,求 LIS
长度。
我们定义 B[i]
来储存可能的上升子序列,len
为 LIS
长度。我们依次把 A[i]
有序地放进 B[i]
里。初始时 B[0] = A[0]
,len = 1
,上升子序列末尾是 3
A | 3 | 1 | 2 | 6 | 4 | 5 | 10 | 7 |
---|---|---|---|---|---|---|---|---|
B | 3 |
(1) A[1] = 1
,因为 1
比 B[]
的末尾 3
小,所以可以把 B[]
中末尾的 3
替换为 1
,此时 B[] = {1}
,len = 1
,上升子序列结尾是 1
A | 3 | 1 | 2 | 6 | 4 | 5 | 10 | 7 |
---|---|---|---|---|---|---|---|---|
B | 1 |
(2) A[2] = 2
,2
大于 B[]
的末尾 1
,就把 2
放进 B[1]
,此时 B[] = {1, 2}
,len = 2
,上升子序列末尾是 2
A | 3 | 1 | 2 | 6 | 4 | 5 | 10 | 7 |
---|---|---|---|---|---|---|---|---|
B | 1 | 2 |
(3) A[3] = 6
,6
大于 B[]
的末尾 2
,把 6
放进 B[2]
,此时 B[ ]={1, 2, 6}
,len = 3
,上升子序列末尾是 6
A | 3 | 1 | 2 | 6 | 4 | 5 | 10 | 7 |
---|---|---|---|---|---|---|---|---|
B | 1 | 2 | 6 |
(4) A[4] = 4
,4
比 B[]
的末尾 6
小,因此需要找到 B[]
中第一个比 4
大的元素,并替换。这也是一种 贪心 策略,在保证当前上升子序列长度不变的前提下,让序列中的元素尽可能地小,那么子序列长度就更可能变大。因为 B[]
中元素已经有序,所以利用二分查找,找到 B[]
中第一个比 4
大的元素 B[2]
,并把 B[2]
替换为 4
,B[] = {1, 2, 4}
,len = 3
,上升子序列末尾是 4
A | 3 | 1 | 2 | 6 | 4 | 5 | 10 | 7 |
---|---|---|---|---|---|---|---|---|
B | 1 | 2 | 4 |
(5) A[5] = 5
,5
大于 B[]
的末尾 4
,把 5
放进 B[3]
,此时 B[] = {1, 2, 4, 5}
,len = 4
,上升子序列末尾是 5
A | 3 | 1 | 2 | 6 | 4 | 5 | 10 | 7 |
---|---|---|---|---|---|---|---|---|
B | 1 | 2 | 4 | 5 |
(6) A[6] = 10
,10
大于 B[]
的末尾 5
,把 10
放进 B[4]
,此时 B[] = {1, 2, 4, 5, 10}
,len = 5
,上升子序列末尾是 10
A | 3 | 1 | 2 | 6 | 4 | 5 | 10 | 7 |
---|---|---|---|---|---|---|---|---|
B | 1 | 2 | 4 | 5 | 10 |
(7) A[8] = 7
,4
比 B[]
的末尾 10
小,因此需要找到 B[]
中第一个比 7
大的元素,并替换。替换后 B[] = {1, 2, 4, 5, 7}
,len = 5
,上升子序列末尾是 7
A | 3 | 1 | 2 | 6 | 4 | 5 | 10 | 7 |
---|---|---|---|---|---|---|---|---|
B | 1 | 2 | 4 | 5 | 7 |
在这个例子中有两个长度相同的最长上升子序列:{1, 2, 4, 5, 10}
和 {1, 2, 4, 5, 7}
,最后一步 7
替换 10
并没有增加最长上升子序列的长度,但这一步的意义在于记录 “最小序列”,它代表了一种 “最可能性”。假如后面还有两个数据 8
和 9
,那么 B[5]
将更新为 8
,B[6]
将更新为 9
,len
就变为 7
private int binarySearch(int[] arr, int low, int high, int target) {
int l = low, r = high - 1;
while (l < r) {
int mid = (l + r) >>> 1;
// 找到第一个 >= target的位置
if (arr[mid] < target) {
l = mid + 1;
} else {
r = mid;
}
}
// 如果target不是一定存在的话需要添加判断条件
return l;
}
public int longestIncreasingSubsequence(int[] nums) {
if (nums == null || nums.length == 0)
return 0;
int n = nums.length;
int[] arr = new int[n]; // 保存可能的上升序列
arr[0] = nums[0];
int res = 1; //初始时 LIS 长度为1
for (int i = 1; i < n; ++i) {
if (nums[i] > arr[res - 1]) {
// 将nums[i]添加到可能的上升序列中
arr[res++] = nums[i];
} else {
//否则,找到arr中第一个 >= nums[i]的位置 pos,用nums[i]更新 arr[pos]
int pos = binarySearch(arr, 0, res, nums[i]);
arr[pos] = nums[i];
}
}
return res;
}
复杂度
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
遍历所有元素 O ( n ) O(n) O(n),二分查找 O ( l o g n ) O(logn) O(logn),
空间复杂度: O ( n ) O(n) O(n)
2.最长连续上升子序列
LeetCode题目链接:674. 最长连续递增序列
题目描述
给定一个未经排序的整数数组,找到最长且 连续 的递增序列。
示例 1:
输入: [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为5和7在原数组里被4隔开。
示例 2:
输入: [2,2,2,2,2]
输出: 1
解释: 最长连续递增序列是 [2], 长度为1。
dp 解法
public int findLengthOfLCIS(int[] nums) {
if (nums == null || nums.length == 0)
return 0;
int res = 1, cur = 1; //cur记录当前最长的连续上升子序列长度
for (int i = 1; i < nums.length; i++) {
if (nums[i - 1] < nums[i])
res = Math.max(res, ++cur);
else cur = 1;
}
return res;
}
复杂度
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
3. 最长递增子序列的个数
LeetCode题目链接:673. 最长递增子序列的个数
题目描述
给定一个未排序的整数数组,找到最长递增子序列的个数。
示例 1:
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
dp 解法
len[i]
:以 nums[i]
为结尾的序列中最长的递增子序列长度
cnt[i]
:以 nums[i]
为结尾的序列中,长度为 len[i]
的递增子序列个数
求 len[i]
方法跟上述 最长递增子序列
问题中的 dp 解法一样。只是需要注意 cnt[]
数组的更新( 假定已经满足 i < j
,nums[i] < nums[j]
)
len[i] >= len[j]
: 这种情形说明nums[i]
和nums[j]
属于同一个递增子序列,并且len[j]
还不是最终结果,nums[j]
尚未加入到这个子序列中。因此更新len[j] = len[i] + 1
,cnt[j] = cnt[i]
len[i] < len[j]
并且len[i] + 1 == len[j]
: 这种情形说明len[j]
已经确定,又出现了以nums[j]
结尾的长度为len[j]
新的递增子序列
public int findNumberOfLIS(int[] nums) {
if (nums == null || nums.length == 0)
return 0;
int n = nums.length;
int[] len = new int[n];
int[] cnt = new int[n];
for (int j = 0; j < n; ++j) {
len[j] = 1;
cnt[j] = 1;
for (int i = 0; i < j; ++i) {
if (nums[i] < nums[j]) {
if (len[i] >= len[j]) {
len[j] = len[i] + 1;
cnt[j] = cnt[i];
} else if (len[i] + 1 == len[j]) {
// 相同长度的递增序列
cnt[j] += cnt[i];
}
}
}
}
int longest = 0, res = 0;
for (int l : len)
longest = Math.max(longest, l);
for (int i = 0; i < n; ++i) {
if (len[i] == longest)
res += cnt[i];
}
return res;
}
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n ) O(n) O(n)