CDQ分治与整体二分 学习笔记
前言:因为我觉得CDQ分治和整体二分很像,也是一起学的,所以决定写一篇博客一起总结一下。部分内容借鉴洛谷日报第115期,感谢。
-------------------------------
CDQ分治与整体二分对于强制在线的问题无能为力。但是当解决一些可以离线的问题时就可以把诸如树套树等数据结构吊起来打。
CDQ分治
讲到CDQ分治就要提到经典的偏序问题。
1.一维偏序:直接$sort$即可。
2.二维偏序:
这样的问题形如“$X_i<X_j,Y_i<Y_j$的点对有多少个”。例如求逆序对就是一个经典的二维偏序问题。
二维偏序的解法是:第一维用$sort$,第二维用数据结构/归并排序来维护。
下面是树状数组求逆序对的代码:
#include<bits/stdc++.h> #define int long long using namespace std; int tree[500005],n,a[500005],b[500005],ans; inline int lowbit(int x){return x&(-x);}//普通的树状数组 inline void add(int x,int y) { while(x<=n) { tree[x]+=y; x+=lowbit(x); } } inline int sum(int x) { int ans=0; while(x>0) { ans+=tree[x]; x-=lowbit(x); } return ans; } signed main() { cin>>n; for (int i=1;i<=n;i++) cin>>a[i],b[i]=a[i]; sort(b+1,b+n+1); int len=unique(b+1,b+n+1)-b-1;//此处要进行离散化 for (int i=1;i<=n;i++) { int x=lower_bound(b+1,b+len+1,a[i])-b; add(x,1); ans+=i-sum(x);//已经插入的数减去在自己前面的数(包括自己)就是位置在自己前面却比自己大的数(逆序对) } cout<<ans; return 0; }
3.三维偏序:
其实就是在二维偏序的基础上多增加了一维。
解法通常是这样:第一维用$sort$,第二维用归并排序,第三维用数据结构。(但第二维我还是想用$sort$,图省事,但常数会略大。
我们在归并的时候考虑$[l,mid]$对$[mid+1,r]$的贡献。因为第一维我们已经对属性$a$排过序了,所以第二维无论怎么归并排序,总满足$[mid+1,r]$中的$a$大于$[l,mid]$中的$a$。
在满足前两维都是有序的情况下,第三维可以直接树状数组维护就可以了。
注意题目中说的是$<$还是$\leq$。如果是$\leq$就要进行去重,等到分治完后再统计答案。
【模板】三维偏序:
#include<bits/stdc++.h> #define int long long using namespace std; const int maxn=400005; int n,cnt=1,ans[maxn],m; int tree[maxn]; struct node { int a,b,c,rank,w; }t[maxn],a[maxn]; inline int lowbit(int x){return x&(-x);} inline bool cmp(node x,node y){ if(x.a!=y.a) return x.a<y.a; if(x.b!=y.b) return x.b<y.b; return x.c<y.c; } inline int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();} while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();} return x*f; } inline void add(int x,int y) { while(x<=m) { tree[x]+=y; x+=lowbit(x); } } inline int sum(int x) { int res=0; while(x>0) { res+=tree[x]; x-=lowbit(x); } return res; } inline void CDQ(int l,int r) { int mid=(l+r)>>1; if (l==r) return; CDQ(l,mid);CDQ(mid+1,r); int p=l,q=mid+1,tot=l; while(p<=mid&&q<=r)//归并排序 { if (a[p].b<=a[q].b) add(a[p].c,a[p].w),t[tot++]=a[p++]; //如果b[p]<=b[q],那么它将对 c[p]<c[q]产生贡献 else a[q].rank+=sum(a[q].c),t[tot++]=a[q++];//后面全都比b[q]大了,直接查询答案 } while(p<=mid) add(a[p].c,a[p].w),t[tot++]=a[p++]; while(q<=r) a[q].rank+=sum(a[q].c),t[tot++]=a[q++]; for (int i=l;i<=mid;i++) add(a[i].c,-a[i].w);//清空树状数组 for (int i=l;i<=r;i++) a[i]=t[i]; } signed main() { n=read();m=read(); for (int i=1;i<=n;i++) a[i].a=read(),a[i].b=read(),a[i].c=read(),a[i].w=1; sort(a+1,a+n+1,cmp); for(int i=2;i<=n;i++){ if(a[i].a==a[cnt].a&&a[i].b==a[cnt].b&&a[i].c==a[cnt].c) a[cnt].w++;//去重 else a[++cnt]=a[i]; } CDQ(1,cnt); for (int i=1;i<=cnt;i++) ans[a[i].rank+a[i].w-1]+=a[i].w;//相等的数互相也有贡献 for (int i=0;i<n;i++) printf("%d\n",ans[i]); return 0; }
【CQOI2011】动态逆序对
这道题也算是一道模板题。我们把删除时间改为插入时间,这样题意就变成了$time_i<time_j,pos_i<pos_j,val_i>val_j$或者$time_i<time_j,pos_i>pos_j,val_i<val_j$的点对数量。
然后就可以CDQ分治搞了。第一维排序$time$,第二维维护$pos$,然后数据结构维护$val$。这里第二维我还是用$sort$,因为归并排序写挂了QAQ。
代码:
#include<bits/stdc++.h> #define int long long using namespace std; const int maxn=100005; int n,m,ans[maxn],res,tmp[maxn],tim,tree[maxn]; struct node { int flag,time,val,pos; }a[maxn],b[maxn]; inline int lowbit(int x){return x&(-x);} inline bool cmpt(node x,node y){return x.time<y.time;} inline bool cmpp(node x,node y){return x.pos<y.pos;} inline int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();} while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();} return x*f; } inline void add(int x,int y) { while(x<=n) { tree[x]+=y; x+=lowbit(x); } } inline int sum(int x) { int ans=0; while(x>0) { ans+=tree[x]; x-=lowbit(x); } return ans; } inline void CDQ(int l,int r) { int mid=(l+r)>>1,cnt=0; if (l>=r) return; for (int i=l;i<=mid;i++) b[++cnt]=a[i],b[cnt].flag=0; for (int i=mid+1;i<=r;i++) b[++cnt]=a[i],b[cnt].flag=1; sort(b+1,b+cnt+1,cmpp); for (int i=1;i<=cnt;i++) { if (b[i].flag==0) add(b[i].val,1); else ans[b[i].time]+=sum(n)-sum(b[i].val); } for (int i=1;i<=cnt;i++) if (b[i].flag==0) add(b[i].val,-1); for (int i=cnt;i>=1;i--) { if (b[i].flag==0) add(b[i].val,1); else ans[b[i].time]+=sum(b[i].val-1); } for (int i=cnt;i>=1;i--) if (b[i].flag==0) add(b[i].val,-1); CDQ(l,mid); CDQ(mid+1,r); } signed main() { n=read(),m=read();tim=n; for (int i=1;i<=n;i++) { int x=read(); a[i].pos=i;a[i].val=x;tmp[x]=i; } for (int i=1;i<=m;i++) { int x=read(); a[tmp[x]].time=tim--; } for (int i=1;i<=n;i++) if (a[i].time==0) a[i].time=tim--; sort(a+1,a+n+1,cmpt); CDQ(1,n); for (int i=1;i<=n;i++) res+=ans[i]; for (int i=n;i>n-m;i--) printf("%lld\n",res),res-=ans[i]; return 0; }
整体二分
整体二分类似于一些决策单调性的分治,能够解决一些诸如区间第$k$小和第$k$大的问题。
有$solve(l,r,L,R)$,表示答案在$[l,r]$范围内,解决$[L,R]$之内的操作。
我们以查询区间第$k$小为例。如果原序列的数$\leq mid$,那么树状数组的相应位置$+1$。如果遇到询问操作,那么查询询问区间$[ql,qr]$中值在$[l,mid]$的数的个数。如果个数$\leq k$那么答案就在右区间中,把$k$减去所求个数;如果$>k$则答案在左区间。
分左右区间的时候维护两个桶即可。
如果$l=r$那么直接统计答案回溯即可。时间复杂度$O(n\log^2 n)$。
【模板】主席树(用整体二分实现)
#include<bits/stdc++.h> #define inf 1e10 #define int long long using namespace std; const int maxn=800005; int n,m,a[maxn],cnt,ans[maxn],tree[maxn]; struct node { int l,r,k,op,id; }q[maxn],q1[maxn],q2[maxn]; inline int lowbit(int x){return x&(-x);} inline int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();} while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();} return x*f; } inline void add(int x,int y) { while(x<=n) { tree[x]+=y; x+=lowbit(x); } } inline int sum(int x) { int ans=0; while(x>0) { ans+=tree[x]; x-=lowbit(x); } return ans; } inline void solve(int l,int r,int ql,int qr) { if (ql>qr) return; if (l==r) { for (int i=ql;i<=qr;i++) if (q[i].op==2) ans[q[i].id]=l; return; } int mid=(l+r)>>1,cnt1=0,cnt2=0; for (int i=ql;i<=qr;i++) { if (q[i].op==1) { if (q[i].l<=mid) add(q[i].id,q[i].r),q1[++cnt1]=q[i]; else q2[++cnt2]=q[i]; } else { int x=sum(q[i].r)-sum(q[i].l-1); if (q[i].k<=x) q1[++cnt1]=q[i]; else q[i].k-=x,q2[++cnt2]=q[i]; } } for (int i=1;i<=cnt1;i++) if (q1[i].op==1) add(q1[i].id,-q1[i].r); for (int i=1;i<=cnt1;i++) q[ql+i-1]=q1[i]; for (int i=1;i<=cnt2;i++) q[ql+cnt1+i-1]=q2[i]; solve(l,mid,ql,ql+cnt1-1); solve(mid+1,r,ql+cnt1,qr); } signed main() { n=read(),m=read(); for (int i=1;i<=n;i++) a[i]=read(),q[++cnt]=(node){a[i],1,0,1,i}; for (int i=1;i<=m;i++) { int l=read(),r=read(),k=read(); q[++cnt]=(node){l,r,k,2,i}; } solve(-inf,inf,1,cnt); for (int i=1;i<=m;i++) printf("%lld\n",ans[i]); return 0; }
如果带修改怎么办?我们可以把原来的“减去”,然后加上后来的。然后跑一遍整体二分即可。具体实现可以看代码。
Dynamic Rankings
#include<bits/stdc++.h> #define int long long #define inf 1e10 using namespace std; const int maxn=1000005; int n,m,a[maxn],tree[maxn],cnt,tot,ans[maxn]; struct node { int l,r,k,id,op; }q[maxn],q1[maxn],q2[maxn]; inline int lowbit(int x){return x&(-x);} inline int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();} while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();} return x*f; } inline void add(int x,int y) { while(x<=n) { tree[x]+=y; x+=lowbit(x); } } inline int sum(int x) { int ans=0; while(x>0) { ans+=tree[x]; x-=lowbit(x); } return ans; } inline void solve(int l,int r,int ql,int qr) { if (ql>qr) return; if (l==r) { for (int i=ql;i<=qr;i++) if (q[i].op==2) ans[q[i].id]=l; return; } int mid=(l+r)>>1,cnt1=0,cnt2=0; for (int i=ql;i<=qr;i++) { if (q[i].op==1) { if (q[i].l<=mid) q1[++cnt1]=q[i],add(q[i].id,q[i].r); else q2[++cnt2]=q[i]; } else { int x=sum(q[i].r)-sum(q[i].l-1); if (q[i].k<=x) q1[++cnt1]=q[i]; else q[i].k-=x,q2[++cnt2]=q[i]; } } for (int i=1;i<=cnt1;i++) if (q1[i].op==1) add(q1[i].id,-q1[i].r); for (int i=1;i<=cnt1;i++) q[ql+i-1]=q1[i]; for (int i=1;i<=cnt2;i++) q[ql+cnt1+i-1]=q2[i]; solve(l,mid,ql,ql+cnt1-1); solve(mid+1,r,ql+cnt1,qr); } signed main() { n=read(),m=read(); for (int i=1;i<=n;i++) a[i]=read(),q[++cnt]=(node){a[i],1,0,i,1}; for (int i=1;i<=m;i++) { char c;cin>>c; if (c=='Q') { int l=read(),r=read(),k=read(); q[++cnt]=(node){l,r,k,++tot,2}; } else { int x=read(),y=read(); q[++cnt]=(node){a[x],-1,0,x,1}; q[++cnt]=(node){a[x]=y,1,0,x,1}; } } solve(-inf,inf,1,cnt); for (int i=1;i<=tot;i++) printf("%lld\n",ans[i]); return 0; }