高維偏序問題降維的有力武器——cdq分治

問題引入

陌上花開1

若干個元素有三個屬性a,b,ca,b,c,問多少對數對i,j(i,j)滿足 aiaj,bibj,cicja_i\leq a_j , b_i\leq b_j , c_i\leq c_j

分析

如此類問題可以視爲一個多維偏序問題,偏序即滿足自反性,反對稱性,傳遞性。畫成拓撲圖則可被視爲一張DAG。

考慮這個問題的簡單版本,如果只有一個屬性,可以容易得到;如果有兩個屬性,常見的解決方案是對其中一維排序後,樹狀數組維護前綴,按照某一位以每個元素log的複雜度順序處理。

在維度更高的情況下,cdq分治是簡化問題的一個有效手段。

cdq分治的核心思想即以一個log爲代價,降低此類多維偏序一個維度。

預備知識

偏序和全序

偏序即滿足:自反性,反對稱性,傳遞性的二元關係。
全序即滿足:完全性,反對稱性,傳遞性的二元關係。
兩者唯一的區別是第一個性質,但是不難發現,自反性即 x  s.t.  xRx\forall x\ \ s.t. \ \ xRx
完全性即 x,y  s.t.  xRy\forall x,y\ \ s.t. \ \ xRy
即偏序只要自己和自己可比即可,全序則是集合內任意兩個元素可比。

像我們處理了如例題陌上花開1的多屬性比較關係,往往是偏序的,因爲可能出現 ai>bj,bi<bja_i>b_j,b_i<b_j的情況,兩者不可比。而自然數集上的大小關係往往可以被視爲全序關係,因爲任意兩個自然數都可比。

引申一點,在全序關係上我們引入良序的定義:若一個全序集上任意一個非空子集S都有最小元,則稱其爲良序。

逆序對

求逆序對個數事實上可以被視爲一個二位偏序的統計問題。對於序列A上每一個元素 aia_i都有兩個屬性,位置 ii 和大小aia_i,把他們全部揪出來可以形成一個偏序集。那麼逆序對實質就是要求集合內所有滿足如下關係的對ij(i,j)的數量: i<j , ai>aji<j\ ,\ a_i>a_j 也即可比的元素對個數。

兩種常見的計算方案如下: 從後往前求維護樹狀數組求解2,或者歸併排序3

後面我們會講到,歸併排序實質上就是cdq分治在二維偏序上的一種應用。而在更高維的三位偏序上,常常採用cdq分治+樹狀數組的解法。

CDQ分治

在這裏我們忽略一維情形,因爲cdq分治的思想就是降維,一維無維可降故忽略。

二維偏序計數

以逆序對問題爲例,假定這裏讀者已經完全掌握了歸併排序求逆序對3的算法,我們將在此基礎上展開討論。

問題建模

要統計數對個數,一種不重複不遺漏的方案是窮舉數對中的一個元素,判斷另一個元素有幾種合法。我們不妨假設要統計數對 (i,j)(i,j) 的個數,那麼對於元素 jj ,我們要找出所有和他可比的元素。

在這裏要特別明確一點,本問題中討論的偏序,指的是各個元素分別滿足某種全序關係(在只出現的元素集上滿足良序關係)的偏序。比如例題陌上花開1中,各個屬性都是正整數,各個屬性內部爲全序,可排序。

那麼 jj 已經確定,我們歸併排序對問題進行分治,那麼這個 ii 要麼和 jj 分在同一組中,要麼分在另一組中。在常見的分治問題中,往往只需要處理分在同一種中的情況,cdq分治的特性就是將跨組的影響也計算出來,從而將分治的思想引入到偏序計數的問題中。

此時我們在用數據結構保存偏序集中每個元素時,除了要保存元素本身的屬性外,還要保存和該元素有關的有序對的數量。在完成算法後,將他們全部取出來求和就是總對數。

以逆序對問題爲例

我們把整個逆序對的元素抽出來,得到一個二維的偏序集,在幾何上可以表示爲平面上的一堆點。

二維偏序
此時我們使用分治思想,把他們從中間切開,得到左右兩部分,如棕色圖所示,可以遞歸處理。我們不妨假設我們此時考慮某個元素 j=5j=5 ,那麼我們實質上要考慮的是有多少 ii 滿足 i<ji<jai>aja_i>a_j。肉眼觀察可得有兩個,分別是 i=2i=2i=4i=4

在這裏插入圖片描述
此時 i=4i=4 的貢獻在分治處理右半邊的時候可以得到,所有放心遞歸即可。而 i=3i=3 的貢獻還在左半側,所以需要單獨處理。如何處理呢?cdq分治在這裏採用的策略就是先排序,後歸併。

在這裏插入圖片描述
在兩側分別對元素值排序後,實質上我們要統計跨界貢獻的就是粉色區域內元素的個數。這個可以很容易在歸併排序時做到。

for(i=l,j=m+1;j<=r;++j){
        while(a[i].y<=a[j].y&&i<=m){
            add(a[i].z);
            ++i;
        }
        a[j].ans+=ask(a[j].z);
    }

在這個過程中,因爲在當前排序的這一元素上是全序的,所以可以排序後歸併遍歷快速得到結果。我們會發現cdq分治帶來了兩個log,在分治時帶來一個log,在統計跨界元素個數時因爲要排序又帶來一個log。但是因爲這兩個log時平行的,所以只需要一個log。而在歸併排序求逆序對個數這一特殊問題時,因爲歸併排序本身可以得到子問題的有序形式,所以在實際操作的時候可以免去排序這一步。但是在cdq分治這一算法框架中排序這一步是必要的。在文章最後總結時我還會給出一個完整的框架,但是到目前爲止我們應該已經初窺了cdq分治的奇妙而精巧的轉化思想。

更暴力的解法

不難發現其實本問題可以轉化爲一個平面上若干個區域內點計數的問題。一個更加暴力的做法是二維線段樹或者樹套樹維護區間和,暴力查詢即可,複雜度O(nlognlogn)O(n log n log n)

三維偏序計數

本部分內容以例題陌上花開1爲例。

問題建模

先考慮我們的老朋友數形結合,不難發現三位偏序可以把他看成一個立體空間裏點計數的問題。暴力做法不難想到,可以三維8叉樹暴力求解,或者樹套樹套樹把低維數據結構套成高維的。編程複雜度巨大,顯然超出一個智力中等水平的大學生的能力範圍。

但是因爲我們有了cdq分治這一有利武器,我們可以考慮將問題降維。我們對數組進行如下三步操作:

Step1:按某一維排序
Step2:分治解決子問題
Step3:解決跨界問題,累計貢獻

這也是cdq分治的基本框架。我們可以看到,在離線處理答案的時候,是以貢獻累計的方式累加到答案上去的,所以這對問題有一個要求:可獨立的疊加和累計。不難發現,計數問題是滿足這個條件的,在每個點之間可以互相獨立的累計到一起。

算法流程

首先按照a軸從小到大排序,這一就得到了一個類似於逆序對的形式:一維有序,另外兩維度亂七八糟。現在我們考慮每一個元素作爲右元形成的數對,顯然只有他左邊的點纔可能產生貢獻,如此我們我們只要統計兩部分內容:和他分在一組的和在他左邊組的。

和他一組的很好處理,直接遞歸求解即可。
和他不一組的則需要另行處理,這是整個cdq分治中最需要動腦子的部分。

那麼怎麼處理呢?一個平凡的想法是直接把左半部用二維線段樹/樹套數維護,右半部分查詢。或者各自排序再歸併,這樣保證第一維有序(因爲跨過中線,所以就算重排之後a仍然有序)。並且歸併第二維也有序,此時直接使用樹狀數組維護前綴即可。

此處給出例題陌上花開1的AC代碼,結合代碼進一步講解。

#include<bits/stdc++.h>
using namespace std;

const int maxn=1e5+5;

struct node{
    int x,y,z,w,ans;
}a[maxn],b[maxn];

int ans[maxn];
int n,k,nn;
map<node,int> mp;

bool cmpx(node a,node b){
    if(a.x==b.x&&a.y==b.y)return a.z<b.z;
    if(a.x==b.x)return a.y<b.y;
    return a.x<b.x;
}

bool cmpy(node a,node b){
    return a.y==b.y?(a.z<b.z):(a.y<b.y);
}

struct FWT{
    const static int N=maxn<<1;
    int a[N];
    void add(int x,int d){
        while(x<N){
            a[x]+=d;
            x+=x&(-x);
        }
    }
    int ask(int x){
        int res=0;
        while(x){
            res+=a[x];
            x-=x&(-x);
        }
        return res;
    }
}fwt;

void cdq(int l,int r){
    if(l==r)return;
    int m=(l+r)>>1;
    cdq(l,m);
    cdq(m+1,r);
    sort(a+l,a+m+1,cmpy);
    sort(a+m+1,a+r+1,cmpy);
    int i,j;
    for(i=l,j=m+1;j<=r;++j){
        while(a[i].y<=a[j].y&&i<=m){
            fwt.add(a[i].z,a[i].w);
            ++i;
        }
        a[j].ans+=fwt.ask(a[j].z);
    }
    for(j=l;j<i;++j)fwt.add(a[j].z,-a[j].w);
}

int main(){
    ios::sync_with_stdio(0);
    cin>>n>>k;
    for(int i=0;i<n;++i){
        cin>>b[i].x>>b[i].y>>b[i].z;
    }
    sort(b,b+n,cmpx);
    for(int i=0,c=0;i<n;++i){
        ++c;
        if(b[i].x!=b[i+1].x||b[i].y!=b[i+1].y||b[i].z!=b[i+1].z){
            a[nn]=b[i];
            a[nn].w=c;
            ++nn;
            c=0;
        }
    }
    cdq(0,nn-1);
    for(int i=0;i<nn;++i){
        ans[a[i].ans+a[i].w-1]+=a[i].w;
    }
    for(int i=0;i<n;++i){
        cout<<ans[i]<<endl;
    }
}

主函數內的內容是一些去重的預處理操作,注意cdq分治無法處理有重複元素的問題,原因是在進行不當的劃分之後可能漏判一些情況。限於篇幅此處不表,讀者可以自行在低維情況下手玩驗證。在一般的套路中,其他的操作可視題目情況而定,但是對第一維排序必不可少即可,即Step1。

for(int i=0;i<n;++i){
    cin>>b[i].x>>b[i].y>>b[i].z;
}
sort(b,b+n,cmpx);

排序後,則可調用cdq分治,核心代碼如下:

void cdq(int l,int r){
    if(l==r)return;
    int m=(l+r)>>1;
    cdq(l,m);
    cdq(m+1,r);
    sort(a+l,a+m+1,cmpy);
    sort(a+m+1,a+r+1,cmpy);
    int i,j;
    for(i=l,j=m+1;j<=r;++j){
        while(a[i].y<=a[j].y&&i<=m){
            fwt.add(a[i].z,a[i].w);
            ++i;
        }
        a[j].ans+=fwt.ask(a[j].z);
    }
    for(j=l;j<i;++j)fwt.add(a[j].z,-a[j].w);
}

其中,若某一時刻當前區間大小爲1(代碼中均採用閉區間),那麼該空間內部不會產生任何貢獻,可以直接return,此爲遞歸的終止邊界。由此可見cdq分治是有窮的。

若區間大小不爲1,那麼必定可以得到兩個子問題,直接分治解決即可,此爲Step2。

其後則要對跨界內容進行處理,即Step3,首先分別排序,這樣在歸併時得到的序列在第二維上就是有序的了。此時我們要計算左半邊對右半邊的貢獻,那麼第一維度左邊總是在右邊的左邊,分別排序後不會影響此性質,第一維度也相對有序。

這樣看似是降低了兩個維度,但是其實只降低了一個。我們考慮用樹狀數組求逆序對的過程,事實上我們是將下標視爲時間維度,按照時間順序維護樹狀數組和統計答案。對於其他任意的二維偏序問題,我們常見的處理方法是先對其中一維排序,視爲時間序,再用樹狀數組等數據結構維護時間前綴上的信息。而在cdq分治中,我們在分治時保證第一維有序,分別排序保證第二維有序,於是我們按照第二維歸併處理時,實質上是按照第二維時間序在操作。通過把點分爲兩部分,計算左邊對右邊的貢獻,實質上我們在忽略第一維度的情況下得到了第二維的時間序,所以對原問題來說是降低了一個維度。

cdq分治的極限

各個屬性分別滿足全序關係的偏序二元對計數問題,我們上面對cdq分治使用的限制條件似乎太過於苛刻了,是否能夠將其拓展,使得更多的問題可以由cdq分治解決呢?

答案是肯定的。不難發現,cdq分治是一種時間換維度的算法,而其中必須要滿足全序關係的屬性其實有且僅有Step1中被排序的一維。換一個角度看,其實cdq分治做的事情是:用一個log的複雜度,將原問題中的時間維消除,轉化爲一組元素對另一組元素的貢獻計數問題。在此基礎上,我們再進行歸併排序,實質上是再構造一個時間維,而這並不是不可或缺的。

比如在陌上花開1中,我們完全可以先使用cdq分治之後再對左半部分用二維線段樹建樹,再用右半部分的每個數對二位線段樹逐個查詢區間和累計答案。這樣實質上我們做到了將8叉樹(三維線段樹)降維到了4叉樹(二維線段樹)。

而對於其他的問題,其實可以考慮很多種不同的算法和數據結構來維護跨區間的貢獻,而他們很多是不要求滿足偏序/全序關係的,甚至不需要有“序”這個概念,比如有多少對gcd不爲1之類。這和很多人提到的cdq分治是在“序”上亂搞的概念不太一樣。cdq分治不在序上亂搞,而是消滅一組全序屬性。

拓展問題

偏序+全序

考慮如下問題,給出若干個元素,每個元素有屬性a,b,問有多少對數對 (i,j)(i,j) 滿足 aiajbibja_i\leq a_j 且 b_i|b_j
其中 xyx|y 表示 xxyy 的因子。

解決方案比較容易想到,先按照a排序,然後cdq分治。接下來問題就變成如下問題:給定一個集合,裏面若干個元素,每個元素有一個屬性 bb , 若干次查詢,問集合中有多少個數是查詢數的因子。

暴力一點就左邊維護一個hash,右邊枚舉因子。如果有現成的數據結構和算法也可以很輕鬆套上去。

求和+全序

考慮如下問題,給出若干個元素,每個元素有屬性a,b,問有多少對數對 (i,j)(i,j) 滿足 aiajbi+bj=Ka_i\leq a_j 且 b_i+b_j=K

解決方案比較容易想到,先按照a排序,然後cdq分治。接下來問題就變成如下問題:給定一個集合,裏面若干個元素,每個元素有一個屬性 bb , 若干次查詢,查詢數爲 qq ,問集合中有多少元素等於 KqK-q

這樣問題就變得非常簡單,hash即可。

應用場景

根據上述兩個隨手捏的例題,不難得出使用cdq分治的情景:

  • 求解合法二元對個數(如果是更多元的話分治時需要更多分類討論,並不實用,故此處認定只適合二元情形)
  • 有至少一個屬性的排序條件爲全序關係,或者分段全序(只要滿足能夠分治且左右兩邊單向有序即可)
  • 維度較高,簡單的數據結構難以維護高維信息

總結

cdq分治的思想

把高維偏序問題降低一個維度,以一個log爲代價。

cdq分治的套路

先對一個維度排序,接下來分治處理貢獻:分治解決同塊貢獻;完事後因爲第一維度在分治之後已經保證有序,接下來就變成在左邊上統計右邊的情況。如果是三維的,那麼直接排序+歸併即可再減掉一維,這一就減掉了兩維,非常輕鬆的解決掉3維偏序的情況。如果是4維,可以無腦再套一個樹套樹。據說還可以cdq套cdq,但是目前還沒有想明白怎麼搞,先咕。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章