CDQ分治學習筆記

2020fengziyang發表於2023-04-27

CDQ分治學習筆記

前言

之前在gdkoi講解是有人用 \(CDQ\) 分治A了day1 T3。好像分治FFT要用到,而且其他人都學過了,所以蒟蒻再次惡補一手之前的知識點。
\(CDQ\) 顯然是一個人的名字,陳丹琪(NOI2008金牌女選手)。

CDQ分治思想

分治就是分治,“分而治之”的思想。
顯然CDQ分治不是普通的分治
因為這一類問題又有一個共同特點就是 左區間會對右區間產生貢獻
我們一般要先求左區間的答案,然後再求左區間對右區間的貢獻,最後求右區間的答案。
前置知識:歸併排序求逆序對,不會的可以看一下這個
樹狀陣列 也要用到一點。

例題

1、翻轉對

然後我們看一下這一題:翻轉對。
題目傳送門
題⽬描述
給定⼀個⻓度為 \(n\) 的陣列 \(nums\) ,如果 \(i < j\)\(nums[i] > 2 * nums[j]\), 我們就將 \((i , j)\) , 稱作⼀個重要翻轉對。你需要返回給定陣列中的重要翻轉對的數量。
資料規模
\(n ≤ 100000\)

分析

很明顯這還只是普通的分治
這題用一個簡單的歸併排序就可以完成,思路差不多,在歸併排序求逆序對的程式碼稍微改一下,把逆序對變成了翻轉對⽽已。

code

很明顯這是網上抄的,主要是我的力扣賬號忘記密碼了

class Solution{
      private int cnt;
      public int reversePairs(int[] nums){
        int len=nums.length;
        sort(nums, Arrays.copyOf(nums, len),0,len-1);
        return cnt;
    }
    private void sort(int[] src,int[] dest,int s,int e){
        if (s>=e){
            return;
        }
        int mid=(s+e)>>1;
        sort(dest,src,s,mid);
        sort(dest,src,mid+1,e);
        merge(src,dest,s,mid,e);
    }
    private void merge(int[] src,int[] dest,int s,int mid,int e) {
        int i=s,j=mid+1,k=s;
        while (i<=mid&&j<=e){
            if((long)src[i]>2*((long)src[j])){
                cnt+=mid-i+1;
                j++;
            } 
            else{
                i++;
            }
        }
        i=s;
        j=mid+1;
        while(i<=mid&&j<=e){
            if (src[i]<=src[j]){
                dest[k++]=src[i++];
            } 
            else{
                dest[k++]=src[j++];
            }
        }
        while(i<=mid){
            dest[k++]=src[i++];
        }
        while(j<=e){
            dest[k++]=src[j++];
        }
    }
}

P3810 三維偏序(陌上花開)

題目傳送門
這道題應該是學過 \(CDQ\) 分治的人都做過了(畢竟是版題,還是紫色的)。
有 $ n $ 個元素,第 $ i $ 個元素有 $ a_i,b_i,c_i $ 三個屬性,設 $ f(i) $ 表示滿足 $ a_j \leq a_i $ 且 $ b_j \leq b_i $ 且 $ c_j \leq c_i $ 且 $ j \ne i $ 的 \(j\) 的數量。

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

輸入格式

第一行兩個整數 $ n,k $,表示元素數量和最大屬性值。

接下來 $ n $ 行,每行三個整數 $ a_i ,b_i,c_i $,分別表示三個屬性值。

輸出格式

$ n $ 行,第 $ d + 1 $ 行表示 $ f(i) = d $ 的 $ i $ 的數量。

樣例 #1

樣例輸入 #1
10 3
3 3 3
2 3 3
2 3 1
3 1 1
3 1 2
1 3 1
1 1 2
1 2 2
1 3 2
1 2 1
樣例輸出 #1
3
1
3
0
1
0
1
0
0
1

提示

$ 1 \leq n \leq 10^5$,$1 \leq a_i, b_i, c_i \le k \leq 2 \times 10^5 $。

分析

這題是一個三維偏序,我們先看一下把它轉換成二維偏序 \(a_j \leq a_i\) \(b_j \leq b_i\) 時怎麼做。
我們把 \(a\) 為第一關鍵字排序,然後用樹狀陣列動態維護維護 \(b_j \leq b_i\) 的個數,時間複雜度:\(O(n\log_2 n)\)

然後考慮本題。
我們還是將 \(a\) 陣列為第一關鍵字,\(b\) 陣列為第二關鍵字,\(c\) 陣列為第三關鍵字排序。
然後用類似於普通分治的方法,將 \([l , r]\) 分成 \([l , mid]\) \([mid + 1 , r]\) 兩個區間。兩個區間內部按照 \(b\) 陣列為第一關鍵字,\(c\) 陣列為第二關鍵字排序,然後同時遍歷兩個子區間,利⽤樹狀陣列將左區間對右區間的貢獻統計到右區間上。
可能有點難懂,大家可以看一下程式碼 感性 理解一下,還是比較簡單的。

code

#include <bits/stdc++.h>
#define fu(x , y , z) for(int x = y ; x <= z ; x ++)
#define LL long long
#define fd(x , y , z) for(int x = y ; x >= z ; x --)
using namespace std;
const int N = 2e5 + 5;
int n , k , tr[N] , ans[N] , m;
struct note {
    int cnt , a , b , c , ans;
}s1[N] , s2[N];
int read () {
    int val = 0 , fu = 1;
    char ch = getchar ();
    while (ch < '0' || ch > '9') {
        if (ch == '-') fu = -1;
        ch = getchar (); 
    }
    while (ch >= '0' && ch <= '9') {
        val = val * 10 + (ch - '0');
        ch = getchar ();
    }
    return val * fu;
}
bool comp1 (note x , note y) {
    if (x.a != y.a) return x.a < y.a;
    if (x.b != y.b) return x.b < y.b;
    if (x.c != y.c) return x.c < y.c;
}
int lowbit (int x) { return x & (-x); }
bool comp2 (note x , note y) {
    if (x.b != y.b) return x.b < y.b;
    return x.c < y.c; 
}
void Insert (int val , int sum) {
    int i = val;
    while (i <= k) {
        tr[i] += sum;
        i += lowbit(i);
    }
}
int query (int x) {
    int sum = 0;
    while (x) {
        sum += tr[x];
        x -= lowbit (x);
    }
    return sum;
}
void cdq (int l , int r) {
    if (l == r) return;
    int mid = l + r >> 1;
    cdq (l , mid) , cdq (mid + 1 , r);
    sort (s2 + l , s2 + mid + 1 , comp2);
    sort (s2 + mid + 1 , s2 + r + 1 , comp2);
    int j = l;
    fu (i , mid + 1 , r) {
        while (s2[i].b >= s2[j].b && j <= mid) {
            Insert (s2[j].c , s2[j].cnt);
            j ++;
        }
        s2[i].ans += query (s2[i].c);
    }
    fu (i , l , j - 1)
        Insert (s2[i].c , -s2[i].cnt);
} 
int main () {
    n = read () , k = read ();
    fu (i , 1 , n)
        s1[i].a = read () , s1[i].b = read () , s1[i].c = read ();
    sort (s1 + 1 , s1 + n + 1 , comp1);
    int t = 0;
    s1[n + 1] = {0 , 0 , 0 , 0 , 0};
    fu (i , 1 , n) {
        t ++;
        if (s1[i].a != s1[i + 1].a || s1[i].b != s1[i + 1].b || s1[i].c != s1[i + 1].c) {
            s2[++ m] = s1[i];
            s2[m].cnt = t;
            t = 0;
        }
    }
    cdq (1 , m);
    fu (i , 1 , m)
        ans[s2[i].cnt + s2[i].ans - 1] += s2[i].cnt;
    fu (i , 0 , n - 1) 
        printf ("%d\n" , ans[i]);
    return 0;
}