题目地址:
https://www.lintcode.com/problem/longest-increasing-subsequence/description
给定一个数组 A A A,求其(严格)最长上升子序列的长度。
法1:动态规划。设 f [ i ] f[i] f[i]是以 A [ i ] A[i] A[i]结尾的最长的上升子序列的长度,那么要计算 f [ i ] f[i] f[i],如果 A [ i ] A[i] A[i]之前有数字 A [ j ] A[j] A[j]小于 A [ i ] A[i] A[i],则 f [ i ] f[i] f[i]可以更新为(如果更长的话) f [ j ] + 1 f[j]+1 f[j]+1;如果没有数小于 A [ i ] A[i] A[i],则 f [ i ] = 1 f[i]=1 f[i]=1。以这个递推关系即可递推出所有以 A [ i ] A[i] A[i]结尾的最长上升子序列的长度。最后只需要遍历 f f f取最大值即可。代码如下:
public class Solution {
/**
* @param nums: An integer array
* @return: The length of LIS (longest increasing subsequence)
*/
public int longestIncreasingSubsequence(int[] nums) {
// write your code here
if (nums == null || nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
dp[i] = Math.max(dp[i], 1);
}
int res = 0;
for (int i : dp) {
res = Math.max(res, i);
}
return res;
}
}
时间复杂度 O ( n 2 ) O(n^2) O(n2),空间 O ( n ) O(n) O(n)。
法2:贪心法。我们想方设法来记录末尾数尽量小的上升子序列。遇到一个新数的时候,去找一下它能接在谁的后面。整个数组遍历完后,返回得到的最长的上升子序列的长度即可。严格证明附在后面,代码如下:
public class Solution {
/**
* @param nums: An integer array
* @return: The length of LIS (longest increasing subsequence)
*/
public int longestIncreasingSubsequence(int[] nums) {
// write your code here
if (nums == null || nums.length == 0) {
return 0;
}
// f[i]是长度为i + 1的所有子序列中末尾数最小的那个子序列的末尾数
int[] f = new int[nums.length];
int cur = 0;
f[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
int last = findFirstLargerOrEqual(f, cur, nums[i]);
if (last != -1) {
f[last] = nums[i];
} else if (nums[i] > f[cur]) {
f[++cur] = nums[i];
}
}
return cur + 1;
}
// 找在f[0,...,r]中找第一个大于或等于num的数的下标;不存在则返回-1
private int findFirstLargerOrEqual(int[] f, int r, int num) {
int l = 0;
while (l < r) {
int m = l + (r - l >> 1);
if (f[m] < num) {
l = m + 1;
} else {
r = m;
}
}
if (f[l] > num) {
return l;
} else {
return -1;
}
}
}
时间复杂度 O ( n log n ) O(n\log n) O(nlogn),空间 O ( n ) O(n) O(n)。
算法正确性证明:
首先证明
f
[
i
]
f[i]
f[i]确实存储了长度为
i
+
1
i+1
i+1的所有上升子序列中末尾数最小的那个子序列的末尾数,并且若
f
[
i
]
=
0
f[i]=0
f[i]=0,说明遍历到当前数为止,未发现长度为
i
+
1
i+1
i+1的上升子序列。
为了证明这一点,先证
f
f
f是严格单调增的,如若不然,则存在
i
i
i使得
f
[
i
]
≥
f
[
i
+
1
]
f[i]\ge f[i+1]
f[i]≥f[i+1],那么存在以
f
[
i
+
1
]
f[i+1]
f[i+1]结尾的长度为
i
+
2
i+2
i+2的子序列,此子序列的第
i
+
1
i+1
i+1个数是比
f
[
i
]
f[i]
f[i]要小的,这与
f
[
i
]
f[i]
f[i]的定义矛盾了。所以
f
f
f单调增。
接下来使用数学归纳法。一开始遍历数组中第一个数,
f
[
0
]
f[0]
f[0]初始化为数组第一个数,是没有问题的。假设后面某时刻
f
[
0
,
.
.
.
,
i
]
f[0,...,i]
f[0,...,i]都符合定义,接下来遍历到数组中的下一个数比如说是
x
x
x,如果
x
>
f
[
i
]
x>f[i]
x>f[i],那么
x
x
x可以接在长度为
i
+
1
i+1
i+1且末尾数为
f
[
i
]
f[i]
f[i]的子序列后面,成为长度为
i
+
2
i+2
i+2的新的上升子序列,所以
f
[
i
+
1
]
f[i+1]
f[i+1]可以被更新为
x
x
x;否则,则要在
f
[
0
,
.
.
.
,
i
]
f[0,...,i]
f[0,...,i]中寻找第一个大于等于
x
x
x的数
f
[
j
]
f[j]
f[j],并将
f
[
j
]
f[j]
f[j]更新为
x
x
x,理由是,由于
f
f
f单调上升,所以
f
[
j
−
1
]
<
x
f[j-1]<x
f[j−1]<x,所以
x
x
x可以接到以
f
[
j
−
1
]
f[j-1]
f[j−1]结尾的长度为
j
j
j的上升子序列后,这样就得到了一个长度为
j
+
1
j+1
j+1的上升子序列,所以
f
[
j
]
f[j]
f[j]可以更新为
x
x
x(当然如果
j
=
0
j=0
j=0那结论更显然)。而对于
k
<
j
k<j
k<j,
f
[
k
]
f[k]
f[k]都小于
x
x
x,所以无法得到更新;而对于
k
>
j
k>j
k>j,如果
f
[
k
]
f[k]
f[k]更新成为
x
x
x,则会导致可以构造出长度为
j
+
1
j+1
j+1且末尾数小于
x
x
x的上升子序列,与
f
[
j
]
f[j]
f[j]的定义矛盾。所以新遍历一个数后,
f
f
f的定义仍然被保持。由数学归纳法,数组遍历完后,
f
f
f非零数字的个数就是最长上升子序列的长度。
注解:法2本质上是(严格)偏序集分解定理,该定理说,一个(严格)偏序集的最长链的长度等于其最少反链分解的分块个数。上面的做法实际上是在做反链分解。