CDQ分治和三維偏序

逆行伐仙發表於2023-10-17

專題:CDQ 分治

本頁面將完整介紹 CDQ 分治。

簡介

CDQ 分治是一種思想而不是具體的演算法,與動態規劃類似。目前這個思想的擴充十分廣泛,依原理與寫法的不同,大致分為三類:

  • 解決和點對有關的問題。
  • 1D 動態規劃的最佳化與轉移。
  • 透過 CDQ 分治,將一些動態問題轉化為靜態問題。

CDQ 分治的思想最早由 IOI2008 金牌得主陳丹琦在高中時整理並總結,它也因此得名。

解決和點對有關的問題

這類問題多數類似於「給定一個長度為 nn 的序列,統計有一些特性的點對 (i,j)(i,j) 的數量/找到一對點 (i,j)(i,j) 使得一些函式的值最大」。

CDQ 分治解決這類問題的演算法流程如下:

  1. 找到這個序列的中點 ;
  2. 將所有點對 (i,j)(i,j) 劃分為 33 類:
    • 1imid1jmid 的點對;
    • 1imidmid+1jn 的點對;
    • mid+1inmid+1jn 的點對。
  3. 將 (1,n)(1,n) 這個序列拆成兩個序列 (1,mid)(1,mid) 和 (mid+1,n)(mid+1,n) 。此時第一類點對和第三類點對都在這兩個序列之中;
  4. 遞迴地處理這兩類點對;
  5. 設法處理第二類點對。

可以看到 CDQ 分治的思想就是不斷地把點對透過遞迴的方式分給左右兩個區間。

在實際應用時,我們通常使用一個函式 solve(l,r) 處理 lirljr 的點對。上述演算法流 程中的遞迴部分便是透過 solve(l,mid) 與 solve(mid,r) 來實現的。剩下的第二類點對則需要額外設計演算法解決。

典型例題1:LOJ112/洛谷P3810 三維偏序(陌上開花)

分析:

三維偏序(陌上開花)是 CDQ 分治的經典問題。

假設我們現在寫好了 solve (l, r) ,並且透過遞迴搞定了 solve (l, mid) 和 solve(mid+1,r) 。現在我們要做的,就是統計滿足 limidmid+1jr 的點對 (i, j)(i,j) 中,有多個點對還滿足 i<jai<ajbi<bj 的限制條件。

稍微思考一下就會發現,那個 i<j 的限制條件沒啥用了:既然 i 比 mid 小, 比 mid 大,那 i 肯定比 j 要小。 現在還剩下兩個限制條件: ai<aj 與 bi<bj , 根據這個限制條件我們就可以列舉 , 求出有多少個滿足條件的 i。

為了方便列舉,我們把 (l,mid) 和 (mid+1,r) 中的點全部按照 a 的值從小到大排個序。之後我們依次列舉每一 個 j , 把所有 ai<aj 的點 i 全部揷入到某種資料結構裡(這裡我們選擇樹狀陣列)。此時只要查詢樹狀陣列裡有多少個點的 b 值是小於 bj 的,我們就求出了對於這個點 j ,有多少個 ii 可以合法匹配它了。

當我們揷入一個 b 值等於 xx 的點時,我們就令樹狀陣列的 xx 這個位置單點 +1+1,而查詢樹狀陣列裡有多少個點小於 xx 的操作實際上就是在求字首和,只要我們事先對於所有的 bb 值做了離散化,我們的複雜度就是對的。

對於每一個 j,我們都需要將所有 ai<aj 的點 i 揷入樹狀陣列中。由於所有的 i 和 j 都已事先按照 aa 值排好序, 這樣的話只要以雙指標的方式在樹狀陣列裡揷入點,則對樹狀陣列的揷入操作就能從 O(n2) 次降到 O(n) 次。

透過這樣一個演算法流程,我們就用 O(nlogn) 的時間處理完了關於第二類點對的資訊了。此時演算法的時間複雜度 是 T(n)= T( n/2 ) + T( n/2 ) + O( nlogn )= O(nlog2n)。

【三維偏序(陌上開花)-參考程式碼-CDQ分治】

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

typedef long long ll;
const int N=200005;
int n,k,m;

struct node
{
    int x,y,z,id,w;
    bool operator < (const node &A)const{
        if(A.x==x && A.y==y) return z < A.z;
        else if(A.x==x) return y < A.y;
        return x < A.x;
    }
}a[N],b[N],d[N];

int ans[N],c[N],cnt[N];
vector <int> v1,v2[N] ;

int lowbit(int x)
{
    return x & (-x);
}

void add(int x,int v)
{
    for(int i=x;i<N;i+=lowbit(i)) c[i]+=v;
}

int query(int x)
{
    int ans=0;
    for(int i=x;i;i-=lowbit(i)) ans+=c[i];
    return ans;
}

void cdq(int l,int r)
{
    if(l==r) return ;
    int mid=(l+r)>>1;
    cdq(l,mid),cdq(mid+1,r);
    int t1=l,t2=mid+1;
    for(int i=l;i<=r;i++)
    {
        if((t1<=mid && a[t1].y<=a[t2].y) || t2>r)
        {
            add(a[t1].z,a[t1].w);
            b[i]=a[t1++];
        }
        else
        {
            cnt[a[t2].id]+=query(a[t2].z);
            b[i]=a[t2++];
        }
    }
    for(int i=l;i<=mid;i++) add(a[i].z,-a[i].w);
    for(int i=l;i<=r;i++) a[i]=b[i];
}

int main()
{
    cin>>n>>k;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i].x>>a[i].y>>a[i].z;
        a[i].id=i;
    }
    sort(a+1,a+n+1);
    int num=1;
    for(int i=2;i<=n+1;i++)
    {
        if(a[i].x!=a[i-1].x || a[i].y!=a[i-1].y || a[i].z != a[i-1].z)
        {
            d[++m]=a[i-1];
            d[m].w=num;
            num=1;
            v2[a[i-1].id]=v1;
            v1.clear();
        }
        else
        {
            num++;
            v1.push_back(a[i-1].id) ;
        }
    }
    for(int i=1;i<=m;i++) a[i]=d[i];
    cdq(1,m);
    for(int i=1;i<=m;i++)
    {
        int sz=v2[a[i].id].size();
        for(auto v:v2[a[i].id]) cnt[v]+=cnt[a[i].id]+sz;
        cnt[a[i].id]+=sz;
    }
    for(int i=1;i<=n;i++) ans[cnt[i]]++;
    for(int i=0;i<n;i++) cout<<ans[i]<<'\n';
    return 0;
}

 

CDQ分治的限制

  1. 題目允許離線操作
  2. 修改操作對詢問的貢獻獨立,且修改之間互不影響
  3. 修改對答案的貢獻是確定的,與判定標準無關

CDQ分治和整體二分

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

相關文章