1. 程式人生 > 其它 >CDQ分治(初步入門)

CDQ分治(初步入門)

CDQ分治

CDQ分治,傳說中是一個神犇創造的演算法。

在瞭解這種演算法之前,我們有必要了解一下一種基本的思想:分治。

分治介紹

分而治之,將原問題不斷劃分成若干個子問題,直到子問題規模小到足以直接解決

子問題間互相獨立且原問題形式相同,遞迴求解這些子問題,然後將各子問題的解合併得到原問題的解

一般步驟

劃分 Divide

將原問題劃分成若干子問題,子問題間互相獨立且與原問題形式相同

解決 Conquer

遞迴解決子問題(遞迴是彰顯分治優勢的工具,僅僅進行一次分治策略也許看不出優勢,但遞迴劃分到子問題規模足夠小,子問題的解可用常數時間解決)

合併 Merge

將各子問題的解合併得到原問題的解

時間複雜度

直觀估計

分治由以上三部分構成,整體時間複雜度則由這三部分的時間複雜度之和構成

由於遞迴,最終的子問題變得極為簡單,以至於其時間複雜度在整個分治策略上的比重微乎其微。

CDQ分治

CDQ分治是我們處理各類問題的重要武器

它的優勢在於可以頂替複雜的高階資料結構,而且常數比較小;

缺點在於必須離線操作

二維偏序問題

上面介紹了歸併求逆序對的經典問題,我們由此引入二維偏序問題:

給定N個有序對(a,b),求對於每個(a,b),滿足a0 < a且b0 < b的有序對(a0,b0)有多少個

在歸併求逆序對的時候,實際上每個元素是用一個有序對(a,b)表示的,其中a表示陣列中的位置,b表示該位置對應的值

我們求的就是“對於每個有序對(a,b),有多少個有序對(a0,b0)滿足a0 < a且b0 > b”,這就是一個二維偏序問題

注意到在求逆序對的問題中,a元素是預設有序的,即我們拿到元素的時候,陣列中的元素是預設從第一個到最後一個按順序排列的,所以我們才能在合併子問題的時候忽略a元素帶來的影響

因為我們在合併兩個子問題的過程中,左邊區間的元素一定出現在右邊區間的元素之前,即左邊區間的元素的a都小於右邊區間元素的a

那麼對於二維偏序問題,我們在拿到所有有序對(a,b)的時候,先把a元素從小到大排序

這時候問題就變成了“求順序對”,因為a元素已經有序,可以忽略a元素帶來的影響,和“求逆序對”的問題是一樣的。

考慮二維偏序問題的另一種解法,用樹狀陣列代替CDQ分治,即常用的用樹狀陣列求順序對

在按照a元素排序之後,我們對於整個序列從左到右掃描,每次掃描到一個有序對,求出“掃描過的有序對中,有多少個有序對的b值小於當前b值”

然而當b的值非常大的時候,空間和時間上就會吃不消,便可以用CDQ分治代替,就是我們所說的“頂替複雜的高階資料結構”

二維偏序問題的拓展

給定一個N個元素的序列a,初始值全部為0,對這個序列進行以下兩種操作

操作1:格式1 x k,把位置x的元素加上k

操作2:格式為2 x y,求出區間[x,y]內所有元素的和

這是一個經典的樹狀陣列問題

但是我們就是要沒事找事,我們用CDQ分治解決它——帶修改和詢問的問題

我們把ta轉化成一個二維偏序問題,每個操作用一個有序對(a,b)表示,其中a表示操作的時間,b表示操作的位置,時間是預設有序的,所以我們在合併子問題的過程中,就按照b從小到大的順序合併。

首先我們把原數列和1操作都看作是修改操作

詢問操作[l,r]我們拆成兩個:l-1,r

因為我們詢問的是一個區間和,一般的思路就是字首和相減(我們需要具備這樣的思維)

實際上我們這道題也可以這樣,我們按照時間順序進行修改
記錄字首和,當遇到l-1的標記時,我們減去sum(l-1)
遇到r標記時,詢問的處理就完成了

具體流程:

  • 按照id(插入位置)歸併排序

  • 進行左區間的修改

  • 統計右區間的詢問

需要注意的是:

  • 在合併的時候,我們只處理左區間的修改,只統計右區間的查詢因為左區間的修改一定可以影響右區間的查詢這就體現出了CDQ分治的基本思想了

  • 我們把所有操作都記錄到了一個數組中,所以陣列的大小至少要開到500000*3

#include<cstdio>
#include<cstring>
#include<iostream>
#define ll long long

using namespace std;

const int N=5000010;
int n,m,totx=0,tot=0;     //totx是操作的個數,tot詢問的編號 

struct node{
    int type,id;
    ll val;
    bool operator < (const node &a) const   //過載運算子,優先時間排序 
    {
        if (id!=a.id) return id<a.id;
        else return type<a.type;
    }
};
node A[N],B[N];
ll ans[N];

void CDQ(int L,int R)
{
    if (L==R) return;
    int M=(L+R)>>1;
    CDQ(L,M);
    CDQ(M+1,R);
    int t1=L,t2=M+1;
    ll sum=0; 
    for (int i=L;i<=R;i++)
    {
        if ((t1<=M&&A[t1]<A[t2])||t2>R) //只修改左邊區間內的修改值
        {
            if (A[t1].type==1) sum+=A[t1].val;   //sum是修改的總值
            B[i]=A[t1++]; 
        }
        else                         //只統計右邊區間內的查詢結果
        {
            if (A[t2].type==3) ans[A[t2].val]+=sum;
            else if (A[t2].type==2) ans[A[t2].val]-=sum;
            B[i]=A[t2++];
        }
    }
    for (int i=L;i<=R;i++) A[i]=B[i];
}

int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++)
    {
        tot++;
        A[tot].type=1; A[tot].id=i;            //修改操作 
        scanf("%lld",&A[tot].val);
    }
    for (int i=1;i<=m;i++)
    {
        int t;
        scanf("%d",&t);
        tot++;
        A[tot].type=t; 
        if (t==1)
            scanf("%d%lld",&A[tot].id,&A[tot].val);
        else
        {
            int l,r;
            scanf("%d%d",&l,&r);
            totx++; 
            A[tot].val=totx; A[tot].id=l-1;    //詢問的前一個位置 
            tot++; A[tot].type=3; A[tot].val=totx; A[tot].id=r;  //詢問的後端點 
        }
    }
    CDQ(1,tot);
    for (int i=1;i<=totx;i++) printf("%lld\n",ans[i]);
    return 0;
}

三維偏序問題

給定N個有序三元組(a,b,c),求對於每個三元組(a,b,c),有多少個三元組(a0,b0,c0)滿足a0 < a且b0 < b且c0 < c
不用CDQ的演算法,我們就不說了(太麻煩了)

類似二維偏序問題,先按照a元素從小到大排序,這樣我們就可以忽略a元素的影響

然後CDQ分治,按照b元素從小到大進行歸併排序

那c元素我們要怎麼處理呢?

這時候比較好的方案就是藉助權值樹狀陣列,
每次從左邊取出三元組(a,b,c),根據c值在樹狀陣列中進行修改

從右邊的序列中取出三元組(a,b,c)時,在樹狀陣列中查詢c值小於(a,b,c)的三元組的個數

注意,每次使用完樹狀陣列要把樹狀陣列清零

三維偏序問題的拓展

平面上有N個點,每個點的橫縱座標在[0,1e7]之間,有M個詢問,每個詢問為查詢在指定矩形之內有多少個點,矩形用(x1,y1,x2,y2)的方式給出,其中(x1,y1)為左下角座標,(x2,y2)為右上角座標

把每個點的位置變成一個修改操作,用三元組(時間,橫座標,縱座標)來表示,把每個查詢變成二維字首和的查詢
這樣對於只有位於詢問的左下角的修改,才對詢問有影響
操作的時間是預設有序的,分治過程中按照橫座標從小到大排序,用樹狀陣列維護縱座標的資訊