cdq分治学习笔记

\(cdq\) 分治是一种思想而不是具体的算法,依原理与写法的不同,大致分为三类:

  • 解决和点对有关的问题。

  • 1D 动态规划的优化与转移。

  • 通过 \(cdq\) 分治,将一些动态问题转化为静态问题。

解决和点对有关的问题

这类问题多数类似于「给定一个长度为 \(n\) 的序列,统计有一些特性的点对 \((i, j)\) 的数量」或「给定一个长度为n的序列,找到一对点 \((i, j)\) 使得一些函数的值最大」
\(cdq\) 分治解决这类问题的算法流程如下:

1.找到序列中点 \(mid\)(def : 集合 \(L =\) { \(x ∈ N \space | \space 1 \leq x \leq mid\) }, \(R =\) { \(x ∈ N \space | \space mid + 1 \leq x \leq n\) })

2.将 \((i,j)\) 分成3类:
a.\(i, j∈L\)
b.\(i∈L, j∈R\)
c.\(i,j∈R\)

(补充:显然有题目限制 \(i \leq j\))

3.将序列 \((1, n)\) 拆成 \((1, mid)\)\((mid + 1, n)\) 两部分,此时a类点对和c类点对都在这两个序列之中,于是考虑递归处理这两类点对。

4.设法处理b类点对。

其实这种思想本质就是不断递归成左右两个区间处理,而夹在中间的b类点对则需要其他算法实现处理。

例题

【模板】三维偏序(陌上花开)说到 \(cdq\) 分治怎么能忘记我们可爱の陌上开花呢(Ciallo~(∠・ω< )⌒☆)

题意简述

有 $ 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 $ 的数量( \(d\) \(from\) \(0\) \(to\) \(n\) )。

思路

首先我们可以对第一维 \(a\) 进行排序,这样序列后面的数一定大于序列前面的数,我们就可以少处理一维。

接着,我们在处理第二维的时候进行 \(cdq\) 分治。

对于一个 \([l,r]\) 的区间,我们可以如此做:

  • 首先处理 \([l,mid]\)\((mid,r]\)。(\(mid\) 是区间中点)

    • 如果 \(l=r\),那么一定没有贡献,可以直接返回。
  • 然后,我们处理左端点在 \([l,mid]\),右端点在 \((mid,r]\) 的点对的贡献。

    • 由于我们已经按 \(a\) 排好序,所以右区间 \((mid,r]\) 的点的 \(a\) 值一定大于左区间。

    • 此时我们把两个区间分别按照 \(b\) 排序。

      • 这样虽然会打乱区间内 \(a\) 的顺序,但是我们只考虑跨区间的贡献。而且区间内的贡献我们在前面已经算完了,后面只会算更大块区间的两侧贡献,而大块之间的 \(a\) 的顺序并不会被排序影响。
    • 然后我们用树状数组+双指针法处理 \(c\) 的偏序关系。

      • 由于我们只要算跨区间的贡献(\(b_i,c_i\) 都小于 \(b_j,c_j\),且 \(i\) 在左区间、\(j\) 在右区间的点对 \(i,j\) 个数),所以树状数组上第 \(i\) 位存 \(c_x=i\)\(x\) 在左区间出现了多少次。(需要离散化)

      • 我们两个指针 \(x,y\) 分别扫左区间和右区间。当 \(y\) 增加后,我们把满足 \(b_x \le b_y\)\(c_x\) 都加入树状数组。由于 \(b\) 是有序的,所以可以用一个指针扫。等到所有满足条件的 \(c_x\) 都加入树状数组后,我们只需查询树状数组上 \(1 \sim c_y\) 的前缀和即是右端点为 \(y\) 的贡献。

  • 然后我们退回上一层,继续这样操作,直到求出所有答案。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 5;

int n, k;
struct node{
    int a, b, c, ans, same;
    inline bool operator==(const node x)const{
        return (x.a == a && x.b == b && x.c == c);
    }
};
inline bool cmp1(node x, node y){
    return (x.a == y.a) ? ((x.b == y.b) ? x.c < y.c : x.b < y.b) : x.a < y.a;
}
inline bool cmp2(node x, node y){
    return (x.b == y.b) ? x.c < y.c : x.b < y.b;
}
node a[N], rd[N];
namespace BIT{
    int t[N];
    #define lowbit(x) (x & -x)
    inline void add(int x, int v){
        for(int i = x; i <= k; i += lowbit(i)) t[i] += v;
    }
    inline int get(int x){
        int ret = 0;
        for(int i = x; i; i -= lowbit(i)) ret += t[i];
        return ret;
    }
    #undef lowbit
};
using namespace BIT;
inline void solve(int l, int r){
    if(l == r) return ;
    int mid = (l + r) >> 1;
    solve(l, mid); solve(mid + 1, r);
    sort(a + l, a + mid + 1, cmp2); sort(a + mid + 1, a + r + 1, cmp2);
    int i = l, j = mid + 1;
	while(j <= r){
		while(i <= mid && a[i].b <= a[j].b) add(a[i].c, a[i].same), i++;
		a[j].ans += get(a[j].c);
        j++;
	}
    for(int m = l; m < i; ++m) add(a[m].c, -a[m].same);
}
int ret[N];

signed main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    cin >> n >> k;
    for(int i = 1; i <= n; i++) cin >> rd[i].a >> rd[i].b >> rd[i].c;
    sort(rd + 1, rd + n + 1, cmp1);
    int cnt = 0, d = n; n = 0;
    for(int i = 1; i <= d; i++){
        ++cnt;
        if(i < d && rd[i] == rd[i + 1]) continue;
        a[++n] = rd[i];
        a[n].same = cnt;
        cnt = 0;
    }
    solve(1, n);
    for(int i = 1; i <= n; i++) ret[a[i].ans + a[i].same - 1] += a[i].same;
    for(int i = 0; i < d; i++) cout << ret[i] << "\n";
    return 0;
}
posted @ 2025-11-19 23:42  WangNoNo  阅读(2)  评论(0)    收藏  举报