Leetcode Algorithm 300. Longest Increasing Subsequence
Longest Increasing Subsequence
给定一个乱序的整形数组,寻找其中最长严格上升子序列的最大长度
解题思路
这是一条经典的动态规划问题,很久之前一次刷题遇到过,但是当时怎么也没弄明白,研究生的算法课上,学到了原来可以用有向无环图的思想来解决。
我们把[10, 9, 2, 5, 3, 7, 101, 18]
这个例子的每个元素用一个节点表示,如果后面某个节点比前面大的话就用一条有向边将它们连起来,从而形成一个有向无环图。
举例来说,与7
结尾的点有[2,5,3]
,那么就是说假如有一个LIS子序列以7
结尾的话,那么它的最大长度依赖于以[2,5,3]
结尾的LIS子序当中的最大长度+1
。这样,我们成功地将原问题分解成一些子问题了。
假设数组是A
,当前节点是i
,那么我们可以列出一条动态转移方程:length[i] = max(length[i], length[k]+1)
,其中k
所有满足A[k]<A[i]
的数。
这个算法的复杂度是 O(n2) 。
有没有更高效率的算法呢?答案是肯定的。其实我们不需要记录以所有节点位结尾的LIS长度,只要保留长度为 l 的最小值就好。
我们用元组(length, value)
来记录长度为
- 插入10:
(1, 10)
- 插入9:
(1, 9)
- 插入2:
(1, 2)
- 插入5:
(1, 2), (2, 5)
- 插入3:
(1, 2), (2, 3)
- 插入7:
(1, 2), (2, 3), (3, 7)
- 插入101:
(1, 2), (2, 3), (3, 7), (4, 101)
- 插入18:
(1, 2), (2, 3), (3, 7), (4, 18)
这样做的子问题的规模比上一种方法要小,而且还可以顺便得出一个满足要求的LIS,而且做插入的时候,还可以用到二分查找(下面的代码中的查找部分是一个线性查找),可以把复杂度降到 O(nlogn) 。
代码
#include<iostream>
#include<vector>
using namespace std;
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if (n == 0)
return 0;
vector<int> length(n, 1);
int max_idx = 0;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
length[i] = max(length[i], length[j] + 1);
}
}
if (length[i] > length[max_idx]) {
max_idx = i;
}
}
return length[max_idx];
}
int lengthOfLISv2(vector<int>& nums) {
int n = nums.size();
if (n == 0)
return 0;
vector<int> list;
list.push_back(nums[0]);
for (int i = 1; i < n; i++) {
int j = list.size() - 1;
// 可以用二分查找,复杂度更低
while (j >= 0 && list[j] >= nums[i]) {
j--;
}
int length = j + 1;
if (length == list.size()) {
list.push_back(nums[i]);
} else {
list[length] = min(list[length], nums[i]);
}
}
return list.size();
}
};
测试样例
int main() {
int a[] = {10, 9, 2, 5, 3, 7, 101, 18};
vector<int> v(a, a + 8);
Solution s;
cout << s.lengthOfLIS(v) << endl;
cout << s.lengthOfLISv2(v) << endl;
return 0;
}
输出
4
4