最长上升子序列问题与最长公共子序列问题的复习
前言:刚学OI时太菜了,没有真正搞懂这些问题,现在希望加深对这些经典问题的理解。
最长上升子序列:
设dp[i]表示长度为i的上升子序列的最小末位数字。显然,长度一定时末尾数字越小越好。然后不断更新最小数字,显然若x>dp[i-1&&x<dp[i]&&x<dp[i+1],则x无法更新dp[i-1],但可以加在dp[i-1]后面构成长度为i的末尾数字更小的子序列。但无法构成一个长度为i+1的上升子序列。因为此前长度为i的上升子序列的末尾最小值都大于x,无法更新。
注意dp数组中存的是长度为i的上升子序列的最小末位数字,而不是最长上升子序列。因为我们加入x是只考虑了前面,没有考虑后面。对于前面,在末尾添加x可以正确的得到序列,但是对于后面的数只有在他加入序列那一刻,它前面的数才是正确的。故输出方案是可以记录加入时前面是那个数。有重复数字时需要记录下标。
代码(输出路径,以UVA-481为例):
#include<bits/stdc++.h>
using namespace std;
#define go(i,a,b) for(int i=a;i<=b;++i)
#define com(i,a,b) for(int i=a;i>=b;--i)
#define mem(a,b) memset(a,b,sizeof(a))
#define fo(i,a) for(int i(0);i<a;++i)
const int inf=0x3f3f3f3f;
const int N=1e6+10;
int n=1,dp[N],a[N],last[N],ans[N];
inline void read(int &x){
x=0;char c=getchar(),f=1;
while(!isdigit(c)){ if(c=='-') f=-1; c=getchar(); }
while(isdigit(c)){ x=x*10+c-'0'; c=getchar(); }
x*=f;
}
struct comp{
bool operator ()(const int &i,const int &j){
return a[i]<a[j];
}
};
int main(){
while(~scanf("%d",&a[n])) ++n;--n;
a[0]=-inf;int len=0;
go(i,1,n){
if(a[i]>a[dp[len]]){
dp[++len]=i;
last[i]=dp[len-1];
}
else{
int p=lower_bound(dp+1,dp+len+1,i,comp())-dp;
dp[p]=i;last[i]=dp[p-1];
}
}
printf("%d\n-\n",len);
int x=dp[len];len=0;
do{
ans[++len]=x;
x=last[x];
}while(x!=0);
com(i,len,1) printf("%d\n",a[ans[i]]);
return 0;
}
最长公共子序列
\(O(n^2)\)做法众所周知,省略。这里主要谈谈\(O(nlogn)\)的做法
注意,该做法仅适用于a,b序列排序后完全相同的情况
算法:
c[i]数组表示a[i]在b数组中的下标,然后对c数组求一个最长上升子序列
正确性证明:
一个公共子序列,满足a[i]=b[j],a[k]=b[l],i<k,j<l。c数组下标表示数字在a中的下标,单调递增。c数组的值表示数字在b中的下标,需要满足单调递增,故问题转化为最长上升子序列。
代码略