CDQ分治總結(CDQ,樹狀數組,歸並排序)


閑話

CDQ是什么?

是一個巨佬,和莫隊、HJT(不是我這個蒟蒻)一樣,都發明出了在OI中越來越流行的算法/數據結構。

CDQ分治思想

分治就是分治,“分而治之”的思想。

那為什么會有CDQ分治這樣的稱呼呢?

這一類分治有一個重要的思想——用一個子問題來計算對另一個子問題的貢獻。

有了這種思想,就可以方便地解決更復雜的問題。

這樣一句話怎樣理解好呢?還是做做題目吧。

例題1

三維偏序問題

洛谷題目傳送門

即給出若干元素,每個元素有三個屬性值\(a,b,c\),詢問對於每個元素\(i\),滿足\(a_j\leq a_i,b_j\leq b_i,c_j\leq c_i\)\(j\)的個數

不用着急,先從簡單的問題開始

試想一下二位偏序也就是\(a_j\leq a_i,b_j\leq b_i\)怎么做

先按\(a\)為第一關鍵字,\(b\)為第二關鍵字排序,那么我們就保證了第一維\(a\)的有序。

於是,對於每一個\(i\),只可能\(1\)\(i-1\)的元素會對它有貢獻,那么直接查\(1\)\(i-1\)的元素中滿足\(b_j\leq b_i\)的元素個數。

具體實現?動態維護\(b\)的樹狀數組,從前到后掃一遍好啦,\(O(n\log n)\)

那么三維偏序呢?我們只有在保證前兩位都滿足的情況下才能計算答案了。

仍然按\(a\)為第一關鍵字,\(b\)為第二關鍵字,\(c\)為第三關鍵字排序,第一維保證左邊小於等於右邊了。

為了保證第二維也是左邊小於等於右邊,我們還需要排序。

想到歸並排序是一個分治的過程,我們可不可以在歸並的過程中,統計出在子問題中產生的對答案貢獻呢?

現在我們有一個序列,我們把它遞歸分成兩個子問題,子問題進行完歸並排序,已經保證\(b\)有序。此時,兩個子問題間有一個分界線,原來第一維左邊小於等於右邊,所以現在分界線左邊的任意一個的\(a\)當然還是都小於右邊的任意一個。那不等於說,只有分界線左邊的能對右邊的產生貢獻?

於是,問題降到了二維。我們就可以排序了,歸並排序(左邊的指針為\(j\),右邊的為\(i\))並維護\(c\)的樹狀數組,如果當前\(b_j\leq b_i\),說明\(j\)可以對后面加入的滿足\(c_j\leq c_i\)\(i\)產生貢獻了,把\(c_j\)加入樹狀數組;否則,因為后面加入的\(j\)都不會對\(i\)產生貢獻了,所以就要統計之前被給的所有貢獻了,查詢樹狀數組\(c_i\)的前綴和。

這是在分治中統計的子問題的答案,跟總答案有怎樣的關系呢?容易發現,每個子問題統計的只有跨越分界線的貢獻,反過來看,每一個能產生貢獻的\(i,j\),有且僅有一個子問題,兩者既同時被包含,又在分界線的異側。那么所有子問題的貢獻加起來就是總答案。

算法的大致思路就是這樣啦。至於復雜度,\(T(n)=O(n\log k)+T(\frac 2 n)=O(n\log n\log k)\)

當然還有不少細節問題。

最大的問題就在於,可能有完全相同的元素。這樣的話,本來它們相互之間都有貢獻,可是cdq的過程中只有左邊的能貢獻右邊的。這可怎么辦呢?

我們把序列去重,這樣現在就沒有相同的了。給現在的每個元素一個權值\(v\)等於出現的次數。中間的具體實現過程也稍有變化,在樹狀數組中插入的值是\(v\)而不是\(1\)了,最后統計答案時,也要算上相同元素內部的貢獻,ans+=v-1

寫法上,為了防止sort和歸並排序中空間移動太頻繁,沒有對每個元素封struct,這樣的話就要膜改一下cmp函數(蒟蒻也是第一次發現cmp可以這么寫)

蒟蒻還是覺得開區間好寫一些吧。。。當然閉區間好理解些。。。

#include<cstdio>
#include<cstring>
#include<algorithm>
#define RG register
#define R RG int
using namespace std;
const int N=1e5+9,SZ=2.2e6;
char buf[SZ],*pp=buf-1;//fread必備
int k,a[N],b[N],c[N],p[N],q[N],v[N],cnt[N],ans[N],*e;
inline int in(){
    while(*++pp<'-');
    R x=*pp&15;
    while(*++pp>'-')x=x*10+(*pp&15);
    return x;
}
void out(R x){
    if(x>9)out(x/10);
    *++pp=x%10|'0';
}
inline bool cmp(R x,R y){//直接對數組排序,注意三關鍵字
    return a[x]<a[y]||(a[x]==a[y]&&(b[x]<b[y]||(b[x]==b[y]&&c[x]<c[y])));
}
inline void upd(R i,R v){//樹狀數組修改
    for(;i<=k;i+=i&-i)e[i]+=v;
}
inline int ask(R i){//樹狀數組查前綴和
    R v=0;
    for(;i;i-=i&-i)v+=e[i];
    return v;
}
void cdq(R*p,R n){//處理一個長度為n的子問題
    if(n==1)return;
    R m=n>>1,i,j,k;
    cdq(p,m);cdq(p+m,n-m);//遞歸處理
    memcpy(q,p,n<<2);//歸並排序
    for(k=i=0,j=m;i<m&&j<n;++k){
        R x=q[i],y=q[j];
        if(b[x]<=b[y])upd(c[p[k]=x],v[x]),++i;//左邊小,插入
        else  cnt[y]+=ask(c[p[k]=y])     ,++j;//右邊小,算貢獻
    }
    for(;j<n;++j)cnt[q[j]]+=ask(c[q[j]]);//注意此時可能沒有完成統計
    memcpy(p+k,q+i,(m-i)<<2);
    for(--i;~i;--i)upd(c[q[i]],-v[q[i]]);//必須這樣還原樹狀數組,memset是O(n^2)的
}
int main(){
    fread(buf,1,SZ,stdin);
    R n=in(),i,j;k=in();e=new int[k+9];
    for(i=0;i<n;++i)
        p[i]=i,a[i]=in(),b[i]=in(),c[i]=in();
    sort(p,p+n,cmp);
    for(i=1,j=0;i<n;++i){
        R x=p[i],y=p[j];++v[y];//模仿unique雙指針去重,統計v
        if(a[x]^a[y]||b[x]^b[y]||c[x]^c[y])p[++j]=x;
    }
    ++v[p[j++]];
    cdq(p,j);
    for(i=0;i<j;++i)
        ans[cnt[p[i]]+v[p[i]]-1]+=v[p[i]];//答案算好
    for(pp=buf-1,i=0;i<n;++i)
        out(ans[i]),*++pp='\n';
    fwrite(buf,1,pp-buf+1,stdout);
}

例題二

洛谷P4169 [Violet]天使玩偶/SJY擺棋子

洛谷題目傳送門

不會KDT,然而CDQ當然是有優勢的。

第一眼就能發現每一個修改或查詢都有三個屬性,\(x,y\),還有時間戳。那么怎樣把它轉化為一般的三維偏序問題呢?

假如所有記憶的點都在查詢的點的左下角,那么就會只有\(x,y\)和時間戳三個維度都小於查詢點的記憶點可以產生貢獻,這就是三維偏序了。

貢獻是什么呢?設有若干\(j\)\(i\)產生了貢獻,那么直接去絕對值,答案就是\(\min\{x_i-x_j+y_i-y_j\}\),也就是\(x_i+y_i-\max\{x_j+y_j\}\),這個還是可以用樹狀數組,只不過改成維護前綴最大值。第一維時間戳,輸入已經排好序了;第二位\(x\)歸並;第三位\(y\)樹狀數組統計答案。

然而假設並不成立。但是我們可以發現,每個能產生貢獻的點只可能會在查詢點的四個方向(左下,左上,右下,右上),那么對所有點還要進行\(3\)遍坐標翻轉(新坐標等於值域減去原坐標),做\(4\)遍CDQ,就可以統計到每個方向的最優答案,最后再取\(\min\)即可。

\(n,m=300000\),值域\(1000000\),一看這\(O((n+m)\log(n+m)\log k)\)好大,還要跑\(4\)遍,真的不會T么?所以還是要優化一些細節。

首先,蒟蒻仍然沿用三維偏序模板的做法,沒有對元素封struct以減少空間交換,這樣在做完坐標翻轉后也能更快還原,直接for一遍初始化。然而也會帶來數組的頻繁調用,蒟蒻也在懷疑這種優化的可行性,還望Dalao指教。

接着,我們發現左邊有\(n\)個元素都確定了是記憶點。也就是說,我們不必對\(n+m\)個點都跑CDQ了,只對后面\(m\)個點跑,前面的\(n\)個點直接預處理按\(x\)第一關鍵字、\(y\)第二關鍵字sort,這樣復雜度就降到了\(O(n\log n+m\log m\log k+n\log k)\)了。然而做完坐標翻轉后也別忘了處理一下。如果這一次翻轉的是\(y\),那么\(x\)不會受到影響;如果翻轉的是\(x\),那么也直接翻轉數組就好啦!

至於fread什么的用上也好。加上這一堆優化,代碼就有90行了。

然后一交上去就1A了!?平時習慣了滿屏殷紅的WA,蒟蒻也不得不感嘆,比起不少數據結構,CDQ真是思路板又好寫還好調。

然而BZOJ的\(n,m\)都有\(500000\),CDQ過不了。。。。。。還是stO洛谷里樓上CDQ跑得超快的巨佬吧,蒟蒻不會卡常。

#include<cstdio>
#include<cstring>
#include<algorithm>
#define RG register
#define I inline void
#define R RG int
#define gc          if(++pp==pe)fread (pp=buf,1,SZ,stdin)
#define pc(C) *pp=C;if(++pp==pe)fwrite(pp=buf,1,SZ,stdout)
const int N=3e5,D=1e6+2,SZ=1<<19,INF=20020307;
char buf[SZ],*pe=buf+SZ,*pp=pe-1;
int x[N],y[N],p[N],q[N],f[N],ans[N],e[D+1];
bool t[N];
struct NODE{
    int x,y;
    inline bool operator<(RG NODE b)const{
        return x<b.x||(x==b.x&&y<b.y);
    }
}a[N];//前n個點
inline int in(){
    gc;while(*pp<'-')gc;
    R x=*pp&15;gc;
    while(*pp>'-'){x*=10;x+=*pp&15;gc;}
    return x;
}
I out(R x){
    if(x>9)out(x/10);
    pc(x%10|'0');
}
I min(R&x,R y){if(x>y)x=y;}
I upd(R i,R v){for(;i<=D;i+=i&-i)if(e[i]<v)e[i]=v;}
I qry(R i,R&v){for(;i   ;i-=i&-i)if(v<e[i])v=e[i];}
I clr(R i)    {for(;i<=D;i+=i&-i)e[i]=0;}
void cdq(R*p,R m){//三維偏序Dalao們都會吧
    if(m==1)return;
    R n=m>>1,i,j,k;
    cdq(p,n);cdq(p+n,m-n);
    memcpy(q,p,m<<2);
    for(k=i=0,j=n;i<n&&j<m;++k)
        if(x[q[i]]<=x[q[j]]){
            if(!t[q[i]])upd(y[q[i]],x[q[i]]+y[q[i]]);
            p[k]=q[i++];
        }
        else{
            if(t[q[j]])qry(y[q[j]],f[q[j]]);
            p[k]=q[j++];
        }
    for(;j<m;++j)
        if(t[q[j]])qry(y[q[j]],f[q[j]]);
    memcpy(p+k,q+i,(n-i)<<2);//注意收尾和清空
    for(--i;~i;--i)clr(y[q[i]]);
}
int main(){
    R n=in(),m=in(),i,j,k;
    for(i=0;i<n;++i)
        a[i].x=in()+1,a[i].y=in()+1;
    std::sort(a,a+n);//n個點預排序
    for(i=0;i<m;++i){
        if((t[i]=in()-1))ans[i]=INF;//注意給極大值
        x[i]=in()+1;y[i]=in()+1;//BIT不能有0下標,所以改一下
    }
    for(k=1;k<=4;++k){
        for(i=0;i<m;++i)p[i]=i;//很快就可以初始化序列
        cdq(p,m);
        for(i=j=0;i<n&&j<m;){//外面統計還是和CDQ一樣,只是不用歸並了
            if(a[i].x<=x[p[j]])
                upd(a[i].y,a[i].x+a[i].y),++i;
            else{
                if(t[p[j]])qry(y[p[j]],f[p[j]]);
                ++j;
            }
        }
        for(;j<m;++j)
            if(t[p[j]])qry(y[p[j]],f[p[j]]);
        memset(e,0,sizeof(e));
        for(i=0;i<m;++i)
            if(t[i]&&f[i])min(ans[i],x[i]+y[i]-f[i]),f[i]=0;
        if(k==4)break;
        if(k&1){//第一次、第三次上下翻
            for(i=0;i<n;++i)a[i].y=D-a[i].y;
            for(i=0;i<m;++i)y[i]=D-y[i];
        }
        else{//第二次左右翻
            for(i=0;i<n;++i)a[i].x=D-a[i].x;
            for(i=0;i<m;++i)x[i]=D-x[i];
            for(i=0,j=n-1;i<j;++i,--j)std::swap(a[i],a[j]);//注意仍要保證x不降
        }
    }
    for(pp=buf,i=0;i<m;++i)
        if(t[i]){out(ans[i]);pc('\n');}
    fwrite(buf,1,pp-buf,stdout);
    return 0;
}


免責聲明!

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



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