1. 程式人生 > >[學習筆記]CDQ分治和整體二分

[學習筆記]CDQ分治和整體二分

序言

\(CDQ\) 分治和整體二分都是基於分治的思想,把複雜的問題拆分成許多可以簡單求的解子問題。但是這兩種演算法必須離線處理,不能解決一些強制線上的題目。不過如果題目允許離線的話,這兩種演算法能把線上解法吊起來打(如樹套樹)。


前置知識:分治

個人覺得分治的經典例子便是歸併排序。

大家都知道,歸併排序就是每次將區間 \([l,r]\) 拆分成 \([l,mid]\)\([mid+1,r]\),然後再 \(O(n)\) 合併兩個有序陣列,再將 \([l,r]\) 的答案傳到上一層去。

那麼我們可以得到 \(T(n)=2\times T(\frac n2)+n\)

因為這樣遞迴層數不會超過 \(\log n\)

層,所以時間複雜度為 \(O(n\log n)\)

void mergesort(int l,int r){
    if(l == r) return ;
    int mid=(l+r)>>1;
    mergesort(l,mid);
    mergesort(mid+1,r);
    int p=l,q=mid+1,cnt=l;
    while(p<=mid&&q<=r){
        if(a[p]<a[q]) t[cnt++]=a[p++];
        else t[cnt++]=a[q++];
    }
    while(p<=mid) t[cnt++]=a[p++];
    while(q<=r) t[cnt++]=a[q++];
    for(int i=l;i<=r;i++) a[i]=t[i];
}

歸併排序的另一個用途:求一個序列的逆序對。

我們發現在合併兩個有序陣列的時候,若 a[p]>a[q] 的時候,那麼 \(a[p]\sim a[mid]\) 的數一定比 \(a[q]\) 大,所以我們在歸併排序的過程中加入一句 if(a[p]>a[q]) ans+=mid-l+1

這種思想在分治中非常有用。


\(CDQ\) 分治

\(CDQ\) 分治的時候就少不了經典的多維偏序問題了。

二維偏序問題

給定 \(n\) 個元素,第 \(i\) 個元素有 \(a_i\)\(b_i\) 兩個屬性,設 \(f(i)\) 表示滿足 \(a_j\leq a_i\)\(b_j\leq b_i\)

\(j\) 的數量。

對於 \(d\in [0,n]\),求滿足 \(f(i)=d\) 的數量。

首先,我們可以把兩個元素抽象成一個點 \((a,b)\),那麼我們就是求一個矩形中有多少個點。

比如我們要求這個矩形內有多少個點:

首先我們可以按照 \(x\) 軸排個序,發現矩形右邊的點已經不在答案的貢獻裡了。那麼 \(f(i)\) 就是在排序後的陣列中找 \(1\sim i-1\) 中有幾個元素 \(b\)\(b_i\) 小。

那麼我們直接樹狀陣列即可,時間複雜度 \(O(n\log n)\)

例題:HDU1541 Stars

#include <bits/stdc++.h>
#define lowbit(x) ((x)&(-(x)))
using namespace std;
const int maxn=100000+10;
int n,c[maxn],ans[maxn];

struct Stars{
    int x,y;
}a[maxn];

bool cmp(Stars a,Stars b){
    if(a.x!=b.x) return a.x<b.x;
    return a.y<b.y;
}

inline int read(){
    register int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
    return (f==1)?x:-x;
}
void add(int x,int y){
    for(;x<maxn;x+=lowbit(x)) c[x]+=y; 
}
int sum(int x){
    int ans=0;
    for(;x;x-=lowbit(x)) ans+=c[x];
    return ans;
}

int main()
{
    n=read();
    for(int i=1;i<=n;i++) 
        a[i].x=read(),a[i].y=read();
    sort(a+1,a+n+1,cmp);
    int now;
    for(int i=1;i<=n;i++){
        now=sum(a[i].y+1);
        ans[now]++;
        add(a[i].y+1,1);
    }
    for(int i=0;i<n;i++) 
        printf("%d\n",ans[i]);
    return 0;
}

自己的題目:【模板】二維偏序

三維偏序問題

其實三維偏序就是在二維偏序上加一維而已。

我們先按每個元素的屬性 \(a\) 排個序,然後第二維用歸併排序,第三維用樹狀陣列。

我們在歸併的時候考慮 \([l,mid]\)\([mid+1,r]\) 的貢獻。因為我們已經按屬性 \(a\) 排過序了,所以在排序屬性 \(b\) 的時候,無論屬性 \(a\) 怎麼被打亂,\([mid+1,r]\) 所有元素的屬性 \(a\) 一定不小於 \([l,mid]\) 中所有元素的屬性 \(a\),所以第二維是成立的。

在滿足前兩維都是有序的時候,類似二維偏序的解法,我們可以用樹狀陣列來統計答案了。

在【模板】三維偏序中,\(a_j\leq a_i\)\(b_j\leq b_i\)\(c_j\leq c_i\) 中是有取等號的,所以我們需要對元素進行去重,最後統計最終的答案,時間複雜度 \(O(n\log^2 n)\)

例題:【模板】三維偏序

#include <bits/stdc++.h>
#define lowbit(x) ((x)&(-(x)))
using namespace std;
const int maxn=100000+10;
int n,m,c[maxn<<1],ans[maxn],cnt;

struct Element{
    int a,b,c,w,f;
}e[maxn],t[maxn];

bool cmp(Element x,Element 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(){
    register int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
    return (f==1)?x:-x;
}

void update(int x,int y){
    for(;x<=m;x+=lowbit(x)) c[x]+=y;
}
int sum(int x){
    int ans=0;
    for(;x;x-=lowbit(x)) ans+=c[x];
    return ans;
}

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(e[p].b<=e[q].b) update(e[p].c,e[p].w),t[tot++]=e[p++];
        else e[q].f+=sum(e[q].c),t[tot++]=e[q++];
    }
    while(p<=mid) update(e[p].c,e[p].w),t[tot++]=e[p++];
    while(q<=r) e[q].f+=sum(e[q].c),t[tot++]=e[q++];
    for(int i=l;i<=mid;i++) update(e[i].c,-e[i].w);
    for(int i=l;i<=r;i++) e[i]=t[i];
}

int main()
{
    n=read();m=read();
    for(int i=1;i<=n;i++)
        e[i].a=read(),e[i].b=read(),e[i].c=read(),e[i].w=1;
    sort(e+1,e+n+1,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]);
    return 0;
}

四維偏序問題

四維偏序就比較噁心了,需要 \(CDQ\)\(CDQ\) 套 樹狀陣列

怎麼叫 \(CDQ\)\(CDQ\) 呢?

我們可以在第二維 \(CDQ\) 的時候,記下那些元素在左區間還在右區間。在第三維 \(CDQ\) 的時候保持前兩維的有序時,加一個樹狀陣列,時間複雜度 \(O(n\log^3 n)\)

例題:HDU5126 stars

不過這道題帶修改,還要判斷是什麼操作。這裡要保證時間的有序和 \(x,y,z\) 三維的有序,所以是四維偏序。

並且求在 \((x_1,y_1,z_1)\)\((x_2,y_2,z_2)\) 的點對個數時要將這些限制拆成八個詢問,容斥一下就好了。

#include <bits/stdc++.h>
#define lowbit(x) ((x)&(-(x)))
using namespace std;
const int maxn=400000+10;
int n,m,mp[maxn],c[maxn],ans[maxn],tot;

struct Element{
    int op,x,y,z,w,id,flag;
}e[maxn],t[maxn],d[maxn];

inline int read(){
    register int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
    return (f==1)?x:-x;
}
void add(int x,int y){
    for(;x<maxn;x+=lowbit(x)) c[x]+=y;
}
int sum(int x){
    int ans=0;
    for(;x;x-=lowbit(x)) ans+=c[x];
    return ans;
}

void CDQ3d(int l,int r){
    if(l==r) return ;
    int mid=(l+r)>>1;
    CDQ3d(l,mid);CDQ3d(mid+1,r);
    int p=l,q=mid+1,cnt=l;
    while(p<=mid&&q<=r){
        if(t[p].y<=t[q].y){
            if(t[p].op==1&&t[p].flag==0)
                add(t[p].z,1);
            d[cnt++]=t[p++];
        }
        else {
            if(t[q].op==2&&t[q].flag==1)
                ans[t[q].id]+=t[q].w*sum(t[q].z);
            d[cnt++]=t[q++];
        }
    }
    while(p<=mid){
        if(t[p].op==1&&t[p].flag==0)
            add(t[p].z,1);
        d[cnt++]=t[p++];
    }
    while(q<=r){
        if(t[q].op==2&&t[q].flag==1)
            ans[t[q].id]+=t[q].w*sum(t[q].z);
        d[cnt++]=t[q++];
    }
    for(int i=l;i<=mid;i++){
        if(t[i].op==1&&t[i].flag==0)
            add(t[i].z,-1);
    }
    for(int i=l;i<=r;i++) t[i]=d[i];
}

void CDQ2d(int l,int r){
    if(l==r) return ;
    int mid=(l+r)>>1;
    CDQ2d(l,mid);CDQ2d(mid+1,r);
    int p=l,q=mid+1,cnt=l;
    while(p<=mid&&q<=r){
        if(e[p].x<=e[q].x){
            t[cnt++]=e[p++];t[cnt-1].flag=0;
        }
        else {
            t[cnt++]=e[q++];t[cnt-1].flag=1;
        }
    }
    while(p<=mid){
        t[cnt++]=e[p++];t[cnt-1].flag=0;
    }
    while(q<=r){
        t[cnt++]=e[q++];t[cnt-1].flag=1;
    }
    for(int i=l;i<=r;i++) e[i]=t[i];
    CDQ3d(l,r);
}

int main()
{
    int T=read();
    while(T--){
        memset(ans,0,sizeof(ans));
        m=read();tot=0;
        int op,x1,y1,z1,x2,y2,z2,t=0;
        for(int i=1;i<=m;i++){
            op=read();
            if(op==1){
                x1=read(),y1=read(),z1=read();
                e[++tot]=(Element){1,x1,y1,z1,1,tot,0};
            }
            else {
                x1=read(),y1=read(),z1=read(),x2=read(),y2=read(),z2=read();
                t++;
                e[++tot]=(Element){2,x2,y2,z2,1,t,0};
                e[++tot]=(Element){2,x1-1,y2,z2,-1,t,0};
                e[++tot]=(Element){2,x2,y1-1,z2,-1,t,0};
                e[++tot]=(Element){2,x2,y2,z1-1,-1,t,0};
                e[++tot]=(Element){2,x1-1,y1-1,z2,1,t,0};
                e[++tot]=(Element){2,x1-1,y2,z1-1,1,t,0};
                e[++tot]=(Element){2,x2,y1-1,z1-1,1,t,0};
                e[++tot]=(Element){2,x1-1,y1-1,z1-1,-1,t,0};
            }
        }
        for(int i=1;i<=tot;i++) mp[i]=e[i].z;
        sort(mp+1,mp+tot+1);
        int cnt=unique(mp+1,mp+tot+1)-mp-1;
        for(int i=1;i<=tot;i++) e[i].z=lower_bound(mp+1,mp+cnt+1,e[i].z)-mp;
        CDQ2d(1,tot);
        for(int i=1;i<=t;i++) printf("%d\n",ans[i]);
    }
    return 0;
}

更高的偏序問題就要用到 \(bitset\) 啦,時間複雜度 \(O(\frac{n^2}{32})\),今天就不講了。

習題:[CQOI2011]動態逆序對

這道題離線做法就是化為 \(Time_i<Time_j\)\(Pos_i<Pos_j\)\(Val_i<Val_j\),然後用 \(CDQ\) 分治解決經典的三維偏序問題


整體二分

整體二分類似於一些決策單調性的分治,可以解決諸多區間第 \(k\) 小或區間第 \(k\) 大的問題。

整體二分 solve(l,r,L,R) 表示答案在 \([l,r]\) 中,與操作 \([L,R]\) 有關(操作 \([L,R]\) 不一定對應原來 \([L,R]\) 的操作)

我們就拿靜態區間第 \(k\) 小來說好了。如果原序列的數 \(\leq mid\),那麼就在樹狀陣列中對應位置 \(+1\)。如果碰到詢問操作,那麼查詢詢問區間 \([ql,qr]\) 的值相當於查詢了區間中值在 \([l,mid]\) 的個數,如果個數 \(\leq k\),那麼答案在 \([mid+1,r]\) 中,那麼把 \(k\) 減掉對應的個數,把操作分到右區間。否則答案在 \([l,mid]\) 中,把操作分到左區間。

如果 \(l=r\),那麼直接把 \(ans\) 記錄一下就好了。時間複雜度 \(O(n\log^2 n)\)

不過我剛剛想到,如果將答案離散化一下,常數理論上會小下很多。讀者有興趣可以實現一下。

例題:【模板】可持久化線段樹 1(主席樹)

#include <bits/stdc++.h>
#define lowbit(x) ((x)&(-(x)))
using namespace std;
const int maxn=200000+10;
const int inf=1e9;
int n,m,a[maxn],c[maxn],ans[maxn],cnt;

struct Query{
    int l,r,k,op,id;
}q[maxn<<1],q1[maxn<<1],q2[maxn<<1];

inline int read(){
    register int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
    return (f==1)?x:-x;
}
void add(int x,int y){
    for(;x<=n;x+=lowbit(x)) c[x]+=y;
}
int sum(int x){
    int ans=0;
    for(;x;x-=lowbit(x)) ans+=c[x];
    return ans;
}

void solve(int l,int r,int L,int R){
    if(L>R) return ;
    if(l==r){
        for(int i=L;i<=R;i++) 
            if(q[i].op==2) ans[q[i].id]=l;
        return ; 
    }
    int mid=(l+r)>>1,cnt1=0,cnt2=0,x;
    for(int i=L;i<=R;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 {
            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[L+i-1]=q1[i];
    for(int i=1;i<=cnt2;i++) q[L+i+cnt1-1]=q2[i];
    solve(l,mid,L,L+cnt1-1);
    solve(mid+1,r,L+cnt1,R);
}

int main()
{
    n=read(),m=read();
    int l,r,k;
    for(int i=1;i<=n;i++) a[i]=read(),q[++cnt]=(Query){a[i],1,0,1,i};
    for(int i=1;i<=m;i++) l=read(),r=read(),k=read(),q[++cnt]=(Query){l,r,k,2,i};
    solve(-inf,inf,1,cnt);
    for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
    return 0;
}

那麼動態區間第 \(k\) 小帶修改操作怎麼搞呢?

我們可以把原來的減掉再加上後來的,然後跑一遍整體二分就好了。

例題:Dynamic Rankings

#include <bits/stdc++.h>
#define lowbit(x) ((x)&(-(x)))
using namespace std;
const int maxn=200000+10;
const int inf=1e9;
int n,m,a[maxn],c[maxn],ans[maxn],cnt,tot;

struct Query{
    int l,r,k,id,op;
}q[maxn*3],q1[maxn*3],q2[maxn*3];

inline int read(){
    register int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
    return (f==1)?x:-x;
}
void add(int x,int y){
    for(;x<=n;x+=lowbit(x)) c[x]+=y;
}
int sum(int x){
    int ans=0;
    for(;x;x-=lowbit(x)) ans+=c[x];
    return ans;
}

void solve(int l,int r,int L,int R){
    if(L > R) return ;
    if(l == r){
        for(int i=L;i<=R;i++) 
            if(q[i].op==2) ans[q[i].id]=l;
        return ; 
    }
    int mid=(l+r)>>1,cnt1=0,cnt2=0,x;
    for(int i=L;i<=R;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 {
            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[L+i-1]=q1[i];
    for(int i=1;i<=cnt2;i++) q[L+i+cnt1-1]=q2[i];
    solve(l,mid,L,L+cnt1-1);
    solve(mid+1,r,L+cnt1,R);
}

int main()
{
    n=read(),m=read();
    int l,r,k;char op;
    for(int i=1;i<=n;i++) a[i]=read(),q[++cnt]=(Query){a[i],1,0,i,1};
    for(int i=1;i<=m;i++){
        op=getchar();
        while(!isalpha(op)) op=getchar();
        if(op=='Q') l=read(),r=read(),k=read(),q[++cnt]=(Query){l,r,k,++tot,2};
        else l=read(),r=read(),q[++cnt]=(Query){a[l],-1,0,l,1},q[++cnt]=(Query){a[l]=r,1,0,l,1};
    }
    solve(-inf,inf,1,cnt);
    for(int i=1;i<=tot;i++) printf("%d\n",ans[i]);
    return 0;
}

習題:[ZJOI2013]K大數查詢

我們把這個區間插入變成線上段樹上區間增減,然後查詢排名就相當於查詢區間和。

不過線段樹清空的時候直接再加一個懶標記在 \(pushdown\) 中下傳就好了,不用像樹狀陣列一樣把原來加上的值減掉。