提示:本文的所有代码由C++实现,使用的开发工具是Visual Studio 2017
前言
本文对Leetcode:300. 最长上升子序列进行了扩展。原文求的是最长上升子序列的长度。本文在求出最长子序列长度之后,对所有最长上升的子序列进行了求解。本文只是给出一种实现方式,并不代表最优方式,欢迎各位同学参与讨论。
一、原题实现
1.题目描述
给定一个无序的整数数组,找到其中最长上升子序列的长度。
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
2.算法实现
使用O(n log n)时间复杂度的算法对该题目进行实现。基本实现思路是使用一个列表记录一个上升序列。如果新元素大于列表尾部元素,则push进入列表,否则使用lower_bound函数搜索>=新元素的位置,并更新该位置的值为新元素。
int findLongestSequence(vector<int> &s1) {
int n = s1.size();
vector<int> box;
for (int i = 0; i < n; i++) {
int c = s1[i];
if (box.empty() || box.back() < c) {
//添加到列表结尾
box.push_back(c);
}
else {
//更新列表位置>=新元素的元素为新元素,不增加列表长度
*lower_bound(box.begin(), box.end(), c) = c;
}
}
return box.size();
3.结果分析
该算法可以很好的把最长上升子序列的长度计算出来,但是box内存储的并不一定是有效的上升子序列。例如输入序列2 5 1 7 9 10 2 3,运行结束时返回的最大长度是5,box内存储的数字是1 2 3 9 10。很明显,box内存储的不是原序列的一个最长上升子序列。为了更有效的获得最大长度后面的2 3对box内数字进行了更新。下面会在该算法的基础上进行扩展,通过dfs搜索所有最长上升子序列。
二、算法扩展
1.增加记忆数组
增加记忆数组rem,用来记录获得最长上升子序列的过程中所有在box中出现的下标。例如输入序列2 5 1 7 9 10 2 3,的有效下标记录为
2 6 7
0 1 3 4 5
int findLongestSequence(vector<int> &s1) {
int n = s1.size();
vector<int> box;
vector<vector<int>> rem; //增加记忆数组
for (int i = 0; i < n;i++) {
int c = s1[i];
if (box.empty() || box.back() < c) {
box.push_back(c);
rem.push_back({i}); //记忆记录新增加元素的下标
}
else {
vector<int>::iterator find = lower_bound(box.begin(), box.end(), c);
*find = c;
rem[find - box.begin()].push_back(i); //记忆数组记录新的下标
}
}
//有效路径搜索部分
vector<vector<int>> res;
vector<int> find;
//对有效路径进行搜索,并记录下标
dfs(s1, rem, res, find);
//调试:展示有效序列下标和有效序列
showRes(s1, res);
return box.size();
}
2.搜索所有有效上升序列
使用dfs搜索所有最长有效上升序列的下标,搜索规则有以下几点。
- 搜索结果长度等于最长子序列长度时,搜索结束
- 新的下标大于搜索队尾下标
- 新的下标对应的数字大于队尾下标对应的数字
/*
s1 原数组
rem 对过程下标的记录数组
res 所有上升序列的返回结果
find 搜索过程保存数组
*/
void dfs(vector<int> &s1, vector<vector<int>> &rem,vector<vector<int>> &res,vector<int> &find) {
int idx = find.size();
if (rem.size() == idx) {
//搜索数组长度等于最大序列长度时,添加该路径
res.push_back(find);
return;
}
for (auto i : rem[idx]) {
//添加到搜索队列的条件,见上面2、3
if (find.empty() || find.back() < i && s1[find.back()]<s1[i]) {
find.push_back(i);
dfs(s1, rem, res, find);
find.pop_back();
}
}
}
3.结果展示
例如输入序列2 5 1 7 9 10 2 3
结果为:
Show result:
Index is:0 1 3 4 5
Squence is:2 5 7 9 10
输入序列2 5 1 7 9 10 2 3 4 5
结果为:
Show result:
Index is:0 1 3 4 5
Squence is:2 5 7 9 10
Index is:2 6 7 8 9
Squence is:1 2 3 4 5
void showRes(vector<int> &s1,vector<vector<int>> &res) {
cout << "Show result:" << endl;
for (auto r : res) {
cout << "Index is:";
for (auto i : r) {
cout << i << " ";
}
cout << endl;
cout << "Squence is:";
for (auto i : r) {
cout << s1[i] << " ";
}
cout << endl;
cout << endl;
}
}
总结
因为有序列长度、下标升序和数字升序的限制,这个dfs的时间复杂度应该不是很高。有兴趣的同学可以帮忙算一下。
三、思路补充(2021-03-19)
最近又学到了一种使用pre数组记录dp路径的方法,因为上面O(nlog(n))的算法会对过程信息压缩。所以想要记录路径需要O(n2)时间复杂度的算法。下面是对该算法的实现。
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0;
vector<int> dp(n,1);
//记录上升序列下标
vector<int> pre(n, -1);
//记录最长上升序列最后的下标
int lastIdx = -1;
int maxLen = 0;
for (int i = 1; i < n; i++) {
for (int j = i - 1; j >= 0; j--) {
if (nums[i] > nums[j] && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
pre[i] = j;
}
}
if (dp[i]>maxLen) {
maxLen = dp[i];
lastIdx = i;
}
}
//通过回溯法查找一个最长上升序列
vector<int> res(maxLen);
int id = maxLen - 1;
while (lastIdx != -1) {
res[id--] = nums[lastIdx];
lastIdx = pre[lastIdx];
}
//打印上升序列
for (auto r : res) {
cout << r << " ";
}
cout << endl;
return maxLen;
}
输入序列2 5 1 7 9 10 2 3 4 5
结果为:
Squence is:2 5 7 9 10
maxLen is:5