1. 程式人生 > >CDQ分治入門

CDQ分治入門

要求 因此 區間 經典 .cn 一個 vat 可能 排序

前言

\(CDQ\)分治是一個神奇的算法。

它有著廣泛的用途,甚至在某些題目中還能取代\(KD-Tree\)樹套樹等惡心的數據結構成為正解,而且常數還小得多。

不過它也有一定的缺點,如必須離線操作,遇到強制在線的題目還是老老實實打樹套樹吧... ...


核心思想

\(CDQ\)分治的核心思想真的是非常簡單,也就是二字(其實所有分治算法都是這樣)。

  • 分: 與常見的二分一樣,將\([l,r]\)區間內的問題分成兩個區間\([l,mid]\)\([mid+1,r]\)解決。
  • 治: \(CDQ\)分治中的這一部分就十分玄學了,它的思想是利用左區間求解右區間,這與普通的分治就大不一樣了。

這樣講畢竟還是十分抽象,讓我們來借助一道經典例題,來粗略地見識一下\(CDQ\)分治的神奇所在。


經典例題:【BZOJ3262】陌上花開

這道題目大致題意就是要你求三維偏序

關於二維偏序

談到三維偏序,我們可能首先會想到二維偏序

或許有些人不知道什麽是二維偏序,但它的另一個名稱——逆序對你總知道吧。

二維偏序一般可以用樹狀數組歸並排序來解決。

關於用樹狀數組,其實我們接下來還要用到。

而對於歸並排序,可以發現它其實也是借助了左區間來求解右區間,或許也能算作一個比較\(Simple\)\(CDQ\)分治?(大霧)

好了,關於逆序對我們就扯到這裏,下面我們來看看如何用\(CDQ\)分治求解三維偏序。

如何求解三維偏序
  • 對於第一維
    • 首先第一步是將數據按照第一維\(x\)進行排序。
    • 這樣就能保證第一維是有序的了。
  • 對於第二維
    • 接下來,在每一次處理完兩個子區間的答案後(註意,一定要先處理子區間,因為接下來的排序會打亂元素的順序),我們再將這兩個子區間分別按照第二維\(y\)排序。
    • 此時,我們依然可以保證,左區間內每個元素的第一維始終小於右區間內每個元素的第一維。(這應該是顯然的吧)
  • 對於第三維
    • 我們可以用\(i\)\(j\)分別記錄右區間左區間當前處理到的點。
    • 對於每一個\(y_j\le y_i\)\(j\),我們可以將其第三維\(z\)加入樹狀數組
    • 由於兩個區間經過排序,\(y\)
      的大小是遞增的,所以\(j\)的大小也是遞增的,這樣就能穩定時間復雜度。
    • 現在,我們已經保證在樹狀數組中的所有元素,它的前兩維皆\(\le\)當前\(i\)的前兩維。因此,我們只要求出有多少個\(z\le z_i\)即可,用樹狀數組可以快速做到這一點。
    • 這樣一來,就能計算出\(i\)的三維偏序數量了。

大致就是這樣一個過程,一次沒有看明白的可以再多看幾遍理解一下。

最後註意一個細節:千萬記得清空樹狀數組!而且千萬記得不要直接\(memset\)


代碼

#include<bits/stdc++.h>
#define max(x,y) ((x)>(y)?(x):(y))
#define min(x,y) ((x)<(y)?(x):(y))
#define uint unsigned int
#define LL long long
#define ull unsigned long long
#define swap(x,y) (x^=y,y^=x,x^=y)
#define abs(x) ((x)<0?-(x):(x))
#define INF 1e9
#define Inc(x,y) ((x+=(y))>=MOD&&(x-=MOD))
#define ten(x) (((x)<<3)+((x)<<1))
#define N 100000
using namespace std;
int n,m,nn;
struct value
{
    int x,y,z,v,tot;
    inline friend bool operator == (value x,value y) {return !(x.x^y.x||x.y^y.y||x.z^y.z);}
}s[N+5];
class FIO
{
    private:
        #define Fsize 100000
        #define tc() (FinNow==FinEnd&&(FinEnd=(FinNow=Fin)+fread(Fin,1,Fsize,stdin),FinNow==FinEnd)?EOF:*FinNow++)
        #define pc(ch) (FoutSize<Fsize?Fout[FoutSize++]=ch:(fwrite(Fout,1,FoutSize,stdout),Fout[(FoutSize=0)++]=ch))
        int f,FoutSize,OutputTop;char ch,Fin[Fsize],*FinNow,*FinEnd,Fout[Fsize],OutputStack[Fsize];
    public:
        FIO() {FinNow=FinEnd=Fin;}
        inline void read(int &x) {x=0,f=1;while(!isdigit(ch=tc())) f=ch^‘-‘?1:-1;while(x=ten(x)+(ch&15),isdigit(ch=tc()));x*=f;}
        inline void read_char(char &x) {while(isspace(x=tc()));}
        inline void read_string(string &x) {x="";while(isspace(ch=tc()));while(x+=ch,!isspace(ch=tc())) if(!~ch) return;}
        inline void write(int x) {if(!x) return (void)pc(‘0‘);if(x<0) pc(‘-‘),x=-x;while(x) OutputStack[++OutputTop]=x%10+48,x/=10;while(OutputTop) pc(OutputStack[OutputTop]),--OutputTop;}
        inline void write_char(char x) {pc(x);}
        inline void write_string(string x) {register int i,len=x.length();for(i=0;i<len;++i) pc(x[i]);}
        inline void end() {fwrite(Fout,1,FoutSize,stdout);}
}F;
inline bool cmp_x(value x,value y) {return x.x^y.x?x.x<y.x:(x.y^y.y?x.y<y.y:x.z<y.z);}//按第一維排序
inline bool cmp_y(value x,value y) {return x.y^y.y?x.y<y.y:x.z<y.z;}//按第二維排序
class Class_CDQ//CDQ分治
{
    private:
        int ans[N+5];//最後統計答案
        class Class_BIT//樹狀數組
        {
            private:
                #define M 200000
                #define lowbit(x) ((x)&-(x))
                int data[M+5];
            public:
                inline void Add(int x,int v) {while(x<=m) data[x]+=v,x+=lowbit(x);}//插入元素
                inline int Query(int x,int ans=0) {while(x) ans+=data[x],x-=lowbit(x);return ans;}//詢問≤x的數的和
        }BIT;
    public:
        inline void Solve(int l,int r)//求解l到r這段區間內的答案
        {
            if(l>=r) return;
            register int mid=l+r>>1,i,j=l;
            Solve(l,mid),Solve(mid+1,r),sort(s+l,s+mid+1,cmp_y),sort(s+mid+1,s+r+1,cmp_y);//切記先求解子區間,然後再排序,排序之後依然能保證右區間第一維大於左區間第一維
            for(i=mid+1;i<=r;++i)
            {
                while(j<=mid&&s[j].y<=s[i].y) BIT.Add(s[j].z,s[j].v),++j;//對於每一個y[j]≤y[i]的j,將z[j]插入樹狀數組
                s[i].tot+=BIT.Query(s[i].z);//求出樹狀數組中≤z[i]的所有元素之和,從而更新i的三維偏序個數
            }
            for(i=l;i<j;++i) BIT.Add(s[i].z,-s[i].v);//切記要這樣清空樹狀數組,memset會T飛(親身實踐)
        }
        inline void PrintAns()
        {
            register int i;
            for(i=1;i<=n;++i) ans[s[i].tot+s[i].v-1]+=s[i].v;//統計答案
            for(i=0;i<nn;++i) F.write(ans[i]),F.write_char(‘\n‘);//輸出
        }
}CDQ;
int main()
{
    register int i;
    for(F.read(nn),F.read(m),i=1;i<=nn;++i) F.read(s[i].x),F.read(s[i].y),F.read(s[i].z),s[i].v=1;
    for(sort(s+1,s+nn+1,cmp_x),i=1;i<=nn;++i) n&&s[n]==s[i]?++s[n].v:(s[++n]=s[i],0);//按照第一維排序,然後去重,從而提高時間效率
    return CDQ.Solve(1,n),CDQ.PrintAns(),F.end(),0;//用CDQ分治求解
}

後記

關於\(CDQ\)分治求解三維偏序,還有一道比較好的題目:【洛谷3157】[CQOI2011] 動態逆序對,可以去做一做。
(這道題卡樹套樹,我的 線段樹套\(Treap\) 只得了\(80\)分,於是為做這題來學了\(CDQ\)分治)

CDQ分治入門