【胡策】一道题(DP+平衡树)
【题意】给定n个数字ai和L,R,ai=0代表在L~R中任取,求最长上升子序列。n<=10^5,0<=ai<=10^5。
【算法】DP+平衡树(fhq-treap)
【题解】对于LIS,有两种经典O(n^2)的递推式,即
①f[i]表示以 i 结尾的LIS,f[i]=max(f[k]+1),k<i&&a[k]<a[i]。
由于本题的数字范围很小,且L~R与数字范围密切相关,所以采用下一种递推式。
②f[i][j]表示前 i 个数以数字 j 结尾的LIS,f[i][ai]=max(f[i-1][k])+1,k<ai,f[i][j]=f[i-1][j],j≠ai。
进一步地,令f[i][j]表示前 i 个数以<=j的数字结尾的LIS,当ai≠0时有:
Ⅰj<ai,f[i][j]=f[i-1][j]
Ⅱj=ai,f[i][ai]=f[i-1][ai-1]+1
Ⅲj>ai,f[i][j]=max(f[i-1][j],f[i][ai]) 本来是f[i][j-1],但实际上只有f[i][ai]改动了。
根据定义,f[i][j],j=1~max(ai)是一个单调不降的序列,这就使ai=0的情况很容易维护(ai≠0视为L=ai=R即可):
Ⅰj<L,f[i][j]=f[i-1][j]
ⅡL<=j<=R,f[i][j]=f[i-1][j-1]+1
Ⅲj>R,f[i][j]=max(f[i-1][j],f[i][R])
省略第一维,第二维的转移用平衡树维护,即:
Ⅰj<L,不变
ⅡL<=j<=R,实际上是从[L-1,R-1]整体平移,只须删除节点R,并在L-1左侧插入原L-1节点,然后对L~R整体+1。
Ⅲj>R,对f[i][R]取max,由于序列单调可以套路化地进行区间覆盖,当然由于只有单点查询也可以直接区间取max。
复杂度O(n log n)。
注意:
1.多标记,一定要注意标记顺序,标记cover时要清除add。
2.t[t[k].l].sz+1
#include<cstdio> #include<cstring> #include<cctype> #include<algorithm> using namespace std; int read(){ char c;int s=0,t=1; while(!isdigit(c=getchar()))if(c=='-')t=-1; do{s=s*10+c-'0';}while(isdigit(c=getchar())); return s*t; } const int maxn=100010; int tot=0,root=0,n,m,L,R,a[maxn]; struct node{int l,r,rnd,delta,cover,num,sz;}t[maxn]; int newnode(int k,int x){t[k]=(node){0,0,rand(),0,0,x,1};return k;} void modify(int k,int x){if(!k)return;t[k].delta+=x;t[k].num+=x;} void Modify(int k,int x){if(!k)return;t[k].cover=x;t[k].num=x;t[k].delta=0;}//more tags void down(int k){ if(!k)return; if(t[k].cover){ Modify(t[k].l,t[k].cover); Modify(t[k].r,t[k].cover); t[k].cover=0; } if(t[k].delta){ modify(t[k].l,t[k].delta); modify(t[k].r,t[k].delta); t[k].delta=0; } } void up(int k){t[k].sz=1+t[t[k].l].sz+t[t[k].r].sz;} int merge(int a,int b){ if(!a||!b)return a^b; if(t[a].rnd<t[b].rnd){ down(a); t[a].r=merge(t[a].r,b); up(a); return a; } else{ down(b); t[b].l=merge(a,t[b].l); up(b); return b; } } void split(int k,int& l,int& r,int x){ if(!k)return void(l=r=0); down(k); if(x<t[t[k].l].sz+1){ r=k; split(t[k].l,l,t[k].l,x); } else{ l=k; split(t[k].r,t[k].r,r,x-t[t[k].l].sz-1); } up(k); } int find(int k,int x){ if(x==t[t[k].l].sz+1)return t[k].num; down(k); if(x<t[t[k].l].sz+1)return find(t[k].l,x);// else return find(t[k].r,x-t[t[k].l].sz-1); } int found(int k,int x){ if(!k)return 0; down(k); if(x<t[k].num)return found(t[k].l,x); else return t[t[k].l].sz+1+found(t[k].r,x); } void work(int l,int r){ int a,b,c,d,e=l-1==0?0:find(root,l-1); split(root,c,d,r); split(c,b,c,r-1); split(b,a,b,l-2); newnode(c,e); down(b);modify(b,1); root=merge(a,c); root=merge(root,b); split(d,a,b,found(d,find(root,r))); down(a);Modify(a,find(root,r)); root=merge(root,a); root=merge(root,b); } int main(){ srand(183); n=read();L=read();R=read();m=R; for(int i=1;i<=n;i++)a[i]=read(),m=max(m,a[i]); root=0; for(int i=0;i<=m;i++)root=merge(root,newnode(++tot,0)); for(int i=1;i<=n;i++){ if(a[i])work(a[i]+1,a[i]+1);else work(L+1,R+1); } printf("%d",find(root,m+1)); return 0; }
差分写法:
在递推式的基础上,可以用平衡树维护差分,即平衡树上的每一点代表f[i]-f[i-1],因为相差只有1,所以每个点只有0和1两种属性。
Ⅰj<L,不变
ⅡL<=j<=R,从[L-1,R-1]整体平移时,内部的差分是不变的,那么在L左侧插入一个节点1(区间+1)。
Ⅲj>R,删除>=R的第一个节点1。