题目描述
给定一个无序的整数数组,找到其中最长上升子序列的长度。
例如:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
解析
暴力解法
本题很容易想到最暴力的解法,那就是找到所有的子序列,逐一判断是否是上升子序列,并记录长度,从而得到最长上升子序列的长度。显然,序列中每个元素都有“加入子序列”或“不加入”两种状态,因此对于长度为N的序列,其子序列数量为2^N(包含空序列),这样的复杂度实在是难以接受。
O(N^2)动态规划解法
由于我们需要求的是最长上升子序列的长度,因此非上升子序列其实并没有必要讨论,而且子序列的元素索引也要保持递增。
因此如果我们想知道以第i个元素结尾的最长上升子序列有多长,就需要遍历0到i-1的元素,如果其中第j个元素小于这里的第i个元素,那意味着以第i个元素结尾的最长上升子序列至少是以第j个元素结尾的最长上升子序列长度再加1.按照顺序方式计算以每个元素结尾的最长上升子序列长度,就得到了时间复杂度为O(N^2)的动态规划解法。
我们需要一个与输入数组等长的数组dp,dp[i]就表示以第i个元素结尾的最长上升子序列的长度。显然dp[0]=1.
状态转移方程为:
d
p
[
i
]
=
m
a
x
(
d
p
[
j
]
)
+
1
,
j
∈
{
0
≤
j
<
i
,
n
u
m
s
[
j
]
<
n
u
m
s
[
i
]
}
dp[i] = max(dp[j])+1, j\in \{0\leq j<i, nums[j]<nums[i]\}
dp[i]=max(dp[j])+1,j∈{0≤j<i,nums[j]<nums[i]}
其中nums为输入的原数组。
最长上升子序列必然以其中一个元素结尾,因此dp数组的最大值即为所求。
根据这一思路,可以写出C++代码如下:
int lenOfLIS(vector<int>&nums) {
if (nums.empty()) return 0;
int len = nums.size();
int maxL = 1;
vector<int>dp(len, 1);
for (int i = 1; i < len; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
maxL = max(maxL, dp[i]);
}
return maxL;
}
O(N*logN)解法
上述动态规划解法关注了以每个元素结尾的最长上升子序列的长度,实际上,我们也可以这样维护一个当前的最长上升子序列,这样我们不需要遍历当前元素之前的所有元素结尾的长度,只需要找到当前元素能否加入这一子序列以及加入的位置,这样遍历全部元素后计算这一子序列长度即可。
首先我们定义一个数组lis, 初始时只包含原数组的第一项nums[0]。接下来遍历原数组,如果遍历到第i个元素,他比lis中的最后一项(最大值)还要大,那么直接在lis尾部插入这一元素;否则,我们找到这个lis数组中大于nums[i]的最小元素(第一个大于nums[i]的元素),并用nums[i]的值替换掉。
这种思路简而言之,就是按顺序遍历数组,如果当前元素大于所维护的上升子序列的最大元素,意味着这个元素可以跟之前的子序列一起构成更长的子序列;反之,可以将维护子序列中的第一个大于该元素的值用这个元素替换掉,意味着这个元素的出现可以在之后的遍历中为插入上升子序列“降低门槛”。
以例题做一个参考:
初始状态
nums: [10,9,2,5,3,7,101,18]
lis: [10]
接下来遍历到数字‘9’由于9<10则10被替换
lis: [9]
然后是’2’,同理替换掉了原来的9
lis: [2]
接下来是5直接插入尾部
lis: [2,5]
接下来的3会替换掉5
lis: [2,3]
遇到7直接插入
lis: [2,3,7]
101直接插入:
lis: [2,3,7,101]
18替换101
lis: [2,3,7,18]
遍历完成,最长子序列可能不唯一最后的lis只是一种结果,但长度都是一样的。
在寻找“大于nums[i]的最小元素”这一过程中,可以通过二分查找将这部分时间复杂度降低到O(logN),因此这个解法总的时间复杂度为O(N*logN)
C++代码如下:
int lenOfLIS2(vector<int>&nums) {
if (nums.empty()) return 0;
int len = nums.size();
vector<int>lis;
lis.push_back(nums[0]);
int maxL = 1;
for (int i = 1; i < len; ++i) {
if (nums[i] > lis.back()) {
lis.push_back(nums[i]);
}
else {
int left = 0, right = maxL - 1, mid;
while (left < right) {
mid = left + (right - left) / 2;
if (lis[mid] < nums[i])left = mid + 1;
else right = mid;
}
lis[right] = nums[i];
}
maxL = lis.size();
}
return maxL;
}