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; }