最长不下降子序列(LIS)
我们知道,递推就是按照递推公式不断往前求解的一个过程,对于当前,只有一个状态描述当前的值。若某些问题并非由一个状态而是由多个状态不断推导,那么这种方法就是动态规划,简称DP。动态规划是运筹学的一个分支,是将问题分解成各个阶段,由相邻的两个阶段根据状态转移方程推到求解的一种方法。
所谓的线性动态规划,就是该问题模型是线性的,数据结构表现为线性表的形式。
题目
题干:
最长不下降子序列。给定一个长度为n的序列a,求出这个序列中最长不下降子序列,所谓最长不下降子序列,就是对于i<j,有a[ i ]<a[ j ]
输入格式:
第一行,一个整数n(n<=5000)
下面n行,没一个整数,其中第i+1行的数为序列a[i]的值。(a[i]<=10^9)
输出格式:
第一行,最长不下降子序列长度
第二行,最长不下降子序列(若有多个,输出一个即可)
输入样例:
8
38 27 55 30 29 70 58 65
输出样例:
3
38 55 70
分析
本题给出的是一个序列,若将该序列的每一个数看成一个点,则数据模型就是一个线性队列。
设f[ i ]表示到当前第i个元素为止最长不下降子序列长度,对于当前的第i个元素,考虑到跟之前哪一个子序列能继续构成不下降序列,从而选取决策。有如下状态转移方程
f[ i ] = max{f[ i ] , f[ i ]+1} 满足 j<i & a[ j ]<=a[ i ]
因为要输出方案,所以在转台转移的时候,需要记录一下这个状态是由哪个状态转移过来的,即记录最优决策。pre[ i ] = j表示当前子序列i的前一个为j
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <set>
#include <string>
#include <queue>
#include <map>
#include <stack>
#include <map>
#include <unordered_map>
#include <vector>
#include <cmath>
#include<string.h>
using namespace std;
#define LL long long
#define Ios ios::sync_with_stdio(false) , cin.tie(0) , cout.tie(0);
#define mem(a,b) memset(a,b,sizeof(a))
const int N=5005;
const int inf=0x3f3f3f3f;
int n,len,a[N],f[N],pre[N],ans[N]; //f为状态描述,pre记录决策值
int main() {
Ios;
cin>>n;
for(int i=1;i<n;i++) cin>>a[i];
a[0] = -inf; //尽管a[0]不存在,因为f[1]要与a[0]比较,因此设为最小
for(int i=1;i<=n;i++){
pre[i] = 0; //初始化决策
for(int j=1;j<i;j++) if(a[j]<=a[i]&&f[j]>f[pre[i]]) pre[i]=j; //记录决策
f[i] = f[pre[i]] + 1; //更新最优值
}
int tail = 0;
for(int i=1;i<=n;i++) if(f[tail]<f[i]) tail = i; //找到最长子序列的位置
for(;tail;tail=pre[tail]) ans[++len]=tail; //将最优子序列逆序放入ans中
cout<<len<<endl; //输出长度
while(len) cout<<a[ans[len--]]<<" "; //输出子序列
cout<<endl;
return 0;
}
优化算法
上述算法时间复杂度为O(N * N) ,对于该题,若数据再大一点,则会超时。我们这里可以利用有序队列优化到时间复杂度为 O(N * logN)
我们再来观察一下这个状态转移方程:
f(i) = max{f(i)+1} 其中 j<i amd a[ j ]<=a[ i ]
上述式子的含义为: 找到i之前的某个j,a[ j ] <= a[ j ], 对于所有的j取 f[ j ] 的最大值。
我们来看下表
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
a[ i ] | 38 | 27 | 55 | 30 | 29 | 70 | 58 | 65 |
f[ i ] | 1 | 1 | 2 | 2 | 2 | 3 | 3 | 4 |
这里的j,是找到1~i之间的所有j,其实这里面有很多是无效的查找,那么我们能否快速查找到我们所要更改的j呢?
更新需要满足两个条件:
- j<i && a [ j ] <= a[ i ]
- f( j )尽可能大
以上两个条件提示我们,后面的值一定要大于等于前面的值。因此我们试着按f[ i ]的增序构建一个a[ i ]不下降的队列。构建过程如下表所示:
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 说明 |
---|---|---|---|---|---|---|---|---|---|
a[ i ] | 38 | 27 | 55 | 30 | 29 | 70 | 58 | 65 | |
i=1 | 38 | f = 1 | |||||||
i=2 | 27 | f =1, 27<38,替换38 | |||||||
i=3 | 27 | 55 | f=2 | ||||||
i=4 | 27 | 30 | f=2,30<55,替换55 | ||||||
i=5 | 27 | 29 | f=2,29<30,替换30 | ||||||
i=6 | 27 | 29 | 70 | f=3 | |||||
i=7 | 27 | 29 | 58 | f=2,58<70,替换70 | |||||
i=8 | 27 | 29 | 58 | 65 | f=4 |
可以看出,每对一次扫描,我们都是再有序队列查找相应的添加或者更新的位置。因此有序队列的二分查找的时间复杂度为 O(logN) ,所以该算法的时间复杂度为 O(N * logN)
优化代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <set>
#include <string>
#include <queue>
#include <map>
#include <stack>
#include <map>
#include <unordered_map>
#include <vector>
#include <cmath>
#include<string.h>
using namespace std;
#define LL long long
#define Ios ios::sync_with_stdio(false) , cin.tie(0) , cout.tie(0);
#define mem(a,b) memset(a,b,sizeof(a))
const int N=100001;
const int inf=0x7f;
int solve(int a[], int n){
int s[N],f[N]; //s[i]存储有序队列的下标,f[s[i]] = i,且a[s[i]]最小
int best,i,low,high,mid;
mem(s,inf);
f[1] = s[1] = best = 1;
for(i=2;i<=n;i++){
//二分查找以a[i]结尾的lIS长度,找到更新位置
for(low=1,high=best;low<=high;(a[a[mid]]<=a[i])?(low=mid+1):(high=mid-1))
mid=(low+high)/2;
//如果f[i]>best则更新best,并设置相应的s[f[i]],否则更新有序队列s[f[i]]
if((f[i]=high+1)>best){
s[best=f[i]]=i;
}else{
s[f[i]] = (a[s[f[i]]] <=a[i])?s[f[i]]:i;
}
}
return best;
}
int main() {
Ios;
int a[N];
int n,i;
scanf("%d",&n);
for(i=1;i<=n;i++){
scanf("%d",&a[i]);
}
printf("%d\n%d\n",solve(a,n));
return 0;
}