问题描述 : 在一个给定的无序序列当中找出最长且递增的子序列 (不一定连续)
对于这个经典问题通常有两种时间复杂度不一样方法来解决一个是O(n2)的算法
另外一个是采用了二分或树状数组O(nlogn)的算法。
动态规划 O(n2) 算法 :
对于序列 squ[1]、squ[2]……squ[n] 分别考虑以每一个元素作为子序列的最后一个元素所能得出的最长长度,使用 dp[i] 来记录这个值 即 dp[i] 表示以第 i 个元素作为子序列的最后一个元素所能组成的最长的递增子序列的最长长度。则初始条件为 dp[1~n] = 1 而后状态转移方程则是 dp[i] = max{ dp[i], dp[j]+1 } 其中 j 的范围为 1~(i-1) 且满足 squ[i] > squ[j]
#include<bits/stdc++.h> using namespace std; const int maxn = 3e3 + 10; int n, len, idx, squ[maxn], dp[maxn], pre[maxn]; inline void LIS() { len = idx = 1;///保存最长长度 以及 最长长度结尾元素的下标 dp[1] = 1; for(int i=2; i<=n; i++){ dp[i] = 1;///初始条件 for(int j=1; j<i; j++){ ///如果要严格递减序列 改成 squ[j] > squ[i] ///如果要不严格上升序列 改成 squ[j] <= squ[i] if(squ[j] < squ[i] && dp[j]+1 > dp[i]){ dp[i] = dp[j]+1; pre[i] = j;///pre数组记录每一个 i 的前驱元素,方便记录序列 } } if(dp[i] > len){ len = dp[i]; idx = i; } } } int main(void) { scanf("%d", &n); for(int i=1; i<=n; i++){ scanf("%d", &squ[i]); pre[i] = i; } LIS(); printf("%d ", len); stack<int> res; while(pre[idx] != idx){///根据子序列最后一个元素下标和pre数组找出前面的节点,用栈存储并输出 res.push(squ[idx]); idx = pre[idx]; } printf("%d ", squ[idx]); while(!res.empty()){ printf("%d ", res.top()); res.pop(); }puts(""); return 0; } /* Input 5 1 2 6 2 4 Output 3 1 2 6 */
二分 O(nlogn) 算法 : (参考博客)
对于序列 squ[1]、squ[2]……squ[n],使用一个 dp[i] 来表示长度为 i 的序列的最后一个元素,注意这里和上面不一样,上面的 dp[i] 值是长度,而这里是具体的元素值是多少,长度则是 i 。那如何做到O(nlogn)呢?下面先来说说做法 :
① 使用 len 来保存当前的最长长度,如果当前元素 squ[i] > dp[len] 也就是比最长长度的最后一个元素还要大则 dp[++len] = squ[i]
② 如果 squ[i] < dp[len] 那么在 dp[1 ~ len-1] 中找到刚好比 squ[i] 大的值将其用 squ[i] 来替换,即 idx = lower_bound(dp+1, dp+1+len, squ[i]) - dp 然后 dp[idx] = squ[i]
第一步好理解,那第二步这种做法的原因是什么呢?
我们先来考虑两个数 x、y 且 x < y 如果当前在 dp[1~(len-1)] 中的任意一个 dp[] 的值是 y ,那么现在进来一个 x 如果使用 x 去代替 y 不会对于当前的最长长度 len 有任何影响,但是会使得当前的序列更有"潜力",例如如果下一个数是 z 且 x < z < y 那么接下来二分找到的便是 x 前面的位置考虑是否被替换成 z ,如果现在进来的是很多个类似于 z 这样的数,那么整个序列每次二分找到的位置都会靠前,在不断替换的情况下就会把之前的 dp[len] 变成更小的值, dp[len] 变得更小,那么 len++ 的可能性就更大,所以将 x 去替换 y 更优。
以下是代码
值得一提,二分的方法在恢复具体的子序列这一方面貌似是有BUG的
所以需要具体的序列建议采用 DP 的写法
#include<bits/stdc++.h> using namespace std; const int maxn = 1e5 + 10; const int INF = 0x3f3f3f3f; int dp[maxn], arr[maxn], N; int strictly_increase() ///求严格上升的子序列、已验证 洛谷OJ ==> P1020 { int Len = 0; memset(dp, 0, sizeof(dp)); dp[Len] = -INF; for(int i=0; i<N; i++){ if(dp[Len] < arr[i]) dp[++Len] = arr[i]; else{ int idx = lower_bound(dp+1, dp+1+Len, arr[i]) - dp; dp[idx] = arr[i]; } } return Len; } int Not_strictly_increase() ///求不严格上升的子序列、未验证 { int Len = 0; memset(dp, INF, sizeof(dp)); dp[Len] = -INF; for(int i=0; i<N; i++){ if(dp[Len] <= arr[i]) dp[++Len] = arr[i]; else{ int idx = upper_bound(dp+1, dp+1+Len, arr[i]) - dp; dp[idx] = arr[i]; } } return Len; } int strictly_decrease() ///求严格下降的子序列、未验证 { int Len = 0; memset(dp, 0, sizeof(dp)); dp[Len] = -INF; for(int i=0; i<N; i++){ if(dp[Len] < -arr[i]) dp[++Len] = -arr[i]; else{ int idx = lower_bound(dp+1, dp+1+Len, -arr[i]) - dp; dp[idx] = -arr[i]; } } return Len; } int Not_strictly_decrease() ///求不严格下降的子序列、已验证 洛谷OJ ==> P1020 { int Len = 0; memset(dp, INF, sizeof(dp)); dp[Len] = -INF; for(int i=0; i<N; i++){ if(dp[Len] <= -arr[i]) dp[++Len] = -arr[i]; else{ int idx = upper_bound(dp+1, dp+1+Len, -arr[i]) - dp; dp[idx] = -arr[i]; } } return Len; } int main(void) { while(true){ scanf("%d", &N); for(int i=0; i<N; i++) scanf("%d", &arr[i]); printf("严格上升的长度 : %d ", strictly_increase()); printf("严格下降的长度 : %d ", strictly_decrease()); printf("不严格上升的长度 : %d ", Not_strictly_increase()); printf("不严格下降的长度 : %d ", Not_strictly_decrease()); } return 0; }
瞎想 : 实际上我感觉 O(nlogn) 的算法不像 dp ,更让我感觉是个贪心构造……
树状数组 O(nlogn) 算法 (参考博客)
使用树状数组来求解 LIS 问题
首先需要说一个前提、树状数组是可以维护前缀的最值的、只要把代码的累加变成维护最值即可
以下的用树状数组解决 LIS 的具体做法
定义树状数组 c[i] 表示以第 i 个数为结尾的 LIS 长度
那么对于第 i 个数的 c[i] 可以从前缀的所有数转移而来 即 c[1~(i-1)]
为了方便转移、我们记录原序列中所有数的位置、然后排序、最后升序去考虑每一个数
文字描述有点难、以下直接给出核心代码去理解
int ans = 0; for(int i=1; i<=N; i++){ int Len = query(num[i].id);///查询在原序列第 num[i].id 数前面的数为结尾的 LIS 长度 add(num[i].id, ++Len);///给当前 num[i].id 为结尾的数的 LIS 长度更新为 ++Len ans = max(ans, Len);///维护最优答案 }
以下是完整代码
#include<bits/stdc++.h> #define lowbit(i) (i&(-i)) using namespace std; const int maxn = 3e3 + 10; struct NUM{ int id, val; bool operator < (const NUM &rhs) const{ if(this->val == rhs.val) return this->id < rhs.id; else return this->val < rhs.val; }; }num[maxn]; int c[maxn]; int N, mx; inline void add(int i, int val) { while(i <= mx){ c[i] = max(val, c[i]); i += lowbit(i); } } int query(int i) { int ret = 0; while(i > 0){ ret = max(ret, c[i]); i -= lowbit(i); } return ret; } int main(void) { mx = -1; scanf("%d", &N); for(int i=1; i<=N; i++) scanf("%d", &num[i].val), num[i].id = i, mx = max(mx, num[i].val); sort(num+1, num+1+N); int ans = 0; for(int i=1; i<=N; i++){ int Len = query(num[i].id);///查询在原序列第 num[i].id 数前面的数为结尾的 LIS 长度 add(num[i].id, ++Len);///给当前 num[i].id 为结尾的数的 LIS 长度更新为 ++Len ans = max(ans, Len);///维护最优答案 } printf("%d ", ans); return 0; }
一些题目
分析 :有一个结论,求最长不严格下降子序列的个数等于求最长上升子序列的个数,后面的就是裸题了
#include<bits/stdc++.h> using namespace std; const int maxn = 1e6 + 10; const int INF = 0x3f3f3f3f; int arr[maxn]; int dp[maxn]; int N; int Not_increase() { int Len = 0; memset(dp, INF, sizeof(dp)); dp[Len] = -INF; for(int i=0; i<N; i++){ if(dp[Len] <= -arr[i]) dp[++Len] = -arr[i]; else{ int idx = upper_bound(dp+1, dp+1+Len, -arr[i]) - dp; dp[idx] = -arr[i]; } } return Len; } int increase() { int Len = 0; memset(dp, 0, sizeof(dp)); dp[Len] = -INF; for(int i=0; i<N; i++){ if(dp[Len] < arr[i]) dp[++Len] = arr[i]; else{ int idx = lower_bound(dp+1, dp+1+Len, arr[i]) - dp; dp[idx] = arr[i]; } } return Len; } int main(void) { N = 0; char ch; while(scanf("%d", &arr[N++])!=EOF); N--; return !printf("%d %d ", Not_increase(), increase()); }