CDQ分治(學習筆記)


離線算法——CDQ分治

 

  CDQ (SHY)顯然是一個人的名字,陳丹琪(MM)(NOI2008金牌選手)。

  •      從歸並開始(這里並沒有從逆序對開始,是想直接引入分治思想,而不是引入處理對象)

  一個很簡單的歸並排序:一個亂序的數列,每次將其折半,類似於線段樹這樣的數據結構,每個子區間先處理好,最后匯總到上一層。

其中層數不超過log(n)層,每次處理的復雜度是O(n)的,因此其復雜度為O(nlogn)。

  code:

void merge_sort(int l,int r) { if(l==r)return; int mid=(l+r>>1); merge_sort(l,mid);merge_sort(mid+1,r); int i=l,j=mid+1,k=l; while(i<=mid&&j<=r) { if(a[i]<a[j])t[k++]=a[i++]; else t[k++]=a[j++]; } while(i<=mid)t[k++]=a[i++]; while(j<=r)t[k++]=a[j++]; go(i,l,r)a[i]=t[i]; }
  •   簡單應用(逆序對)

  逆序對就是求數列中滿足i<j&&a[i]>a[j]的二元組(i,j)的對數。

  舉個栗子(真好吃):1,4,3,8,4,3,8(val)

            1,2,3,4,5,6,7(pos)

  對於pos:(2,3),(2,6),(4,5),(4,6),(5,6)都是逆序對

  於是就我們可以由歸並的性質:因為pos已經天然有序,因此我們只要看val就可以了。

  在merge時,如果右邊的當前位置j比左邊的位置i小,那么它一定比[i,mid]中的所有數都小,因此ans+=mid-i+1。

  •   從逆序對到二維偏序問題

  二維偏序問題其實就是把逆序對的pos打亂,且會重復,它可以理解為在平面直角坐標系中,有n個點(i,j),求每個點和(0,0)形成的矩形內有多少個點。

  雙關鍵字排序后直接樹狀數組即可:

  例題:二維偏序

#include<iostream> #include<algorithm> #include<cstdio> #include<cstring>
#define lowbit(x) (x&-x)
using namespace std; const int N=100010; struct star { int x,y; }s[N]; long long tarr[N]; int n; long long ans; bool cmp(star a,star b) { return a.x==b.x?a.y<b.y:a.x<b.x; } void add(int pos,int val) { while(pos<=N) { tarr[pos]+=val; pos+=lowbit(pos); } } int query(int pos) { int res=0; while(pos) { res+=tarr[pos]; pos-=lowbit(pos); }return res; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d%d",&s[i].x,&s[i].y); } sort(s+1,s+1+n,cmp); for(int i=1;i<=n;i++) { ans+=query(s[i].y); add(s[i].y,1); } cout<<ans; }

 

 

 

 

  •    從二維偏序到三維偏序

  板子:陌上花開

  有了之前的基礎,三維偏序也很簡單:

  我們按三關鍵字排序,(優先級a>b>c),對第二位進行歸並排序,在merge時,對於左邊的i和右邊的j,如果第二維滿足(bi<bj),則在樹狀數組中加入ci,直到存在某個bi不滿足此關系,則j可以查詢樹狀數組中所有ci<cj的三元組,由於之前對第一維的排序和對第二維的歸並,前兩維一定是滿足條件的。這里有個去重還是挺煩的。

code:

#include<iostream> #include<cstdio> #include<algorithm> #include<cstring>
#define lowbit(x) (x&-x)
using namespace std; const int N=100010; const int M=200010; struct node { int a,b,c,f,w; }e[N],t[N]; int cnt; int n,m; int ans[N]; int tarr[M]; bool cmp(node x,node y) { return x.a==y.a?(x.b==y.b?x.c<y.c:x.b<y.b):x.a<y.a; } void add(int pos,int val) { while(pos<=m) { tarr[pos]+=val; pos+=lowbit(pos); } } int query(int pos) { int res=0; while(pos) { res+=tarr[pos]; pos-=lowbit(pos); }return res; } void CDQ(int l,int r) { if(l==r)return; int mid=(l+r>>1); CDQ(l,mid);CDQ(mid+1,r); int i=l,j=mid+1,k=l; while(i<=mid&&j<=r) { if(e[i].b<=e[j].b)add(e[i].c,e[i].w),t[k++]=e[i++]; else e[j].f+=query(e[j].c),t[k++]=e[j++]; } while(i<=mid)add(e[i].c,e[i].w),t[k++]=e[i++]; while(j<=r)e[j].f+=query(e[j].c),t[k++]=e[j++]; for(int i=l;i<=mid;i++)add(e[i].c,-e[i].w); for(int i=l;i<=r;i++)e[i]=t[i]; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d%d%d",&e[i].a,&e[i].b,&e[i].c),e[i].w=1; sort(e+1,e+1+n,cmp); cnt=1; for(int i=2;i<=n;i++) { if(e[i].a==e[cnt].a&&e[i].b==e[cnt].b&&e[i].c==e[cnt].c) e[cnt].w++; else e[++cnt]=e[i]; } CDQ(1,cnt); for(int i=1;i<=cnt;i++)ans[e[i].f+e[i].w-1]+=e[i].w; for(int i=0;i<n;i++)printf("%d\n",ans[i]); }  

 

 

  •   應用:

  Mokia

  這是一個三維偏序問題,我們把時間當做第一維,x當做第二維,y當做第三維。

  按照處理三維偏序的思路,我們先按(t>x>y)排序,對x進行歸並,對y進行樹狀數組。

  但是此題的查詢比較煩:它查詢的是矩陣前綴和。因此對每個查詢,我們要處理四個區間的前綴和。這里我把一次詢問拆成了四次詢問。

  具體細節看code:

  

#include<iostream> #include<algorithm> #include<cstring> #include<cstdio>
#define lowbit(x) (x&-x)
using namespace std; const int W=2000010; const int Q=200010; struct node { int id,x,y,t,sum;//id為時間,t為類型(add還是query),t=0,sum為添加的值, //t=1,sum為返回值 
}e[Q],t[Q]; int cnt; int tarr[W]; inline int read() { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int w; bool cmp(node a,node b) { return a.id==b.id?(a.x==b.x?a.y<b.y:a.x<b.x):a.id<b.id; } bool cmp2(node a,node b) { return a.id<b.id; } void add(int pos,int val) { while(pos<=W) { tarr[pos]+=val; pos+=lowbit(pos); } } int query(int pos) { int res=0; while(pos) { res+=tarr[pos]; pos-=lowbit(pos); }return res; } void CDQ(int l,int r)//CDQ分治和歸並板子其實沒啥本質區別 
{ if(l==r)return; int mid=(l+r>>1); CDQ(l,mid);CDQ(mid+1,r); int i=l,j=mid+1,k=l;//基本操作 
    while(i<=mid&&j<=r) { if(e[i].x<=e[j].x)//對第二維進行歸並,如果滿足條件,把第三維的貢獻放到樹狀數組里 
 { if(e[i].t==0)add(e[i].y,e[i].sum);//此時t需要為0 
            t[k++]=e[i++]; } else { if(e[j].t==1)e[j].sum+=query(e[j].y);//如果已經沒有滿足條件的x了,我們就進行統計 
            t[k++]=e[j++];//此時t需要為1 
 } } while(i<=mid) { if(e[i].t==0) add(e[i].y,e[i].sum); t[k++]=e[i++]; } while(j<=r) { if(e[j].t==1) e[j].sum+=query(e[j].y); t[k++]=e[j++]; }//統計剩下的 
    for(int i=l;i<=mid;i++)if(e[i].t==0)add(e[i].y,-e[i].sum);//必須要清空樹狀數組 
    for(int i=l;i<=r;i++) e[i]=t[i]; } int main() { while(1) { int cid=read(); if(cid==0)w=read(); if(cid==1) { int x=read()+1,y=read()+1,z=read(); e[++cnt]=(node){cnt,x,y,0,z}; //結構體直接讀取 
 } if(cid==2) { int i=read(),j=read(),x=read()+1,y=read()+1;//x,y可能為0,樹狀數組會爆 
            e[++cnt]=(node){cnt,i,j,1,0};//因此它們都要加一,對結果無影響 
            e[++cnt]=(node){cnt,i,y,1,0}; e[++cnt]=(node){cnt,x,j,1,0}; e[++cnt]=(node){cnt,x,y,1,0};//一個詢問拆成四個詢問 
 } if(cid==3)break; } sort(e+1,e+1+cnt,cmp);//對第一維排序 
    CDQ(1,cnt); sort(e+1,e+1+cnt,cmp2);//查詢之前一定要按時間排好序,因為已經按第二維歸並了一遍 
    for(int i=1;i<=cnt;i++) { int ans=0; if(e[i].t==1) { int a=e[i].sum,b=e[i+1].sum,c=e[i+2].sum,d=e[i+3].sum; ans=a-b-c+d;printf("%d\n",ans);//遇到查詢,4個合並起來就是最終答案 
            i+=3; } } }

 

 

 

  二維偏序很好懂,三維偏序太難畫,所以這里就不放圖了

  完美撒花✿✿ヽ(°▽°)ノ✿

  ——Thranduil

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM