CDQ分治-学习总结篇

CDQ分治学习笔记

CDQ分治思想

我们要理解到 CDQ 分治是做什么的,CDQ 分治是一种分治算法,或者说是一种思想,其主要内容是:将序列通过递归的方式分给左右两个区间,每一个子问题只处理跨左右区间的贡献。

使用 CDQ 分治建立在排序的基础上,这也说明 CDQ 分治必须离线使用

CDQ 分治可以解决的问题:

  • 与点对有关的问题(数点 & 偏序)。
  • 将带修改 & 查询的动态问题,转化为静态问题。
  • 优化 1D / 1D 动态规划的转移。

本质上,这些问题都属于“点对之间的贡献问题”。

点对相关知识(可以不看)

二元关系

定义

何为二元关系(binary relation)?感性理解,二元关系就是 function<bool(T1, T2)>,比如说:

  • \(\mathbb{R}\) 上的 \(<\),即 less<double>
  • \(\mathbb{N}_+\) 上的 \(|\),即 [(int x, int y) { return y % x == 0; }]
  • 集合上的 \(\subseteq\)
  • 甚至是 \(\mathbb{Z}\) 上的奇怪关系 [(int x, int y) { return x + y >= 10; }]

注意到以上的四个例子都满足 \(T1 = T2\),事实上,这样的二元关系被称为齐次二元关系

那么我们给出数学上的标准定义:


二元关系(binary relation)

集合 \(X\) 和集合 \(Y\) 上的一个二元关系 \(R\) 定义为元组 \((X, Y, G(R))\),其中:

  • \(X\) 称为定义域(domain),
  • \(Y\) 称为值域(codomain),
  • \(G(R) \subseteq X \times Y = \{(x, y) : x \in X, y \in Y\}\) 称为二元关系 \(R\)(graph)。
    \(xRy\) 成立当且仅当 \((x, y) \in G(R)\)

特别的,若 \(X = Y\),则称该二元关系为齐次二元关系(homogeneous relation)。


下文我们所研究的二元关系,都是齐次二元关系。


直接一点就是用 \(R\) 表示二元关系(即有某种关系/ \(R\) 表示有关系)。(什么,你问我是哪种关系,我也不知道,你自己看题目要求QWQ~~)


性质

研究二元关系时,我们常常关注以下这些特殊的性质:

  1. 自反性(reflexive):
    \((\forall a \in S) \; aRa\)

  2. 反自反性(irreflexive, anti-reflexive):
    \((\forall a \in S) \; \neg(aRa)\)

  3. 对称性(symmetric):
    \((\forall a, b \in S) \; aRb \iff bRa\)

  4. 反对称性(antisymmetric):
    \((\forall a, b \in S) \; (aRb \land bRa) \implies a = b\)

  5. 传递性(transitive):
    \((\forall a, b, c \in S) \; (aRb \land bRc) \implies aRc\)

  6. 连接性(connected):
    \((\forall a, b \in S) \; a \neq b \implies (aRb \lor bRa)\)
    (若 \(aRb \lor bRa\),则称 \(a\)\(b\)可达的。)

  7. 不可比的传递性(transitive of incomparability):
    \((\forall a, b, c \in S) \; \big( \neg(aRb \lor bRa) \land \neg(bRc \lor cRb) \big) \implies \neg(aRc \lor cRa)\)
    (若 \(\neg(aRb \lor bRa)\),则称 \(a\)\(b\)不可比的。)

分类(只讲偏序)

根据各种二元关系所具有的性质,我们可以对二元关系进行分类,最常见的是偏序(partial order)和全序(total order)。

§ 偏序(partial order):满足自反性、反对称性和传递性的二元关系,即:

  • \((\forall a \in S) \, aRa\)
  • \((\forall a, b \in S) \, (aRb \land bRa) \implies a = b\)
  • \((\forall a, b, c \in S) \, (aRb \land bRc) \implies aRc\)

其对应的定义域被称为偏序集。

数点

\(k\) 维数点问题,即在一个由 \(n\)\(k\) 元组构成的集合
\(\{(a_{1,1}, \dots, a_{1,k}), \dots, (a_{n,1}, \dots, a_{n,k})\}\) 中,求满足下列条件的 \(k\) 元组 \((a_{j,1}, \dots, a_{j,k})\) 的个数 / 权值和:

  • \(l_1 \le a_{j,1} \le r_1\)
  • \(\dots\)
  • \(l_n \le a_{j,n} \le r_n\)

可以理解为在 \(k\) 维平面上给定若干点,查询某个区域内点的个数。


通常情况下偏序问题和数点问题是可以互相转换的

三维偏序/陌上花开 题解(CDQ分治解法)

众所周知,这道题可以用无数种办法干过,biteset可以25行过掉,已严肃畏惧。

题意理解

n个元素,每个元素有a、b、c三个特征值,\(f(i)\) 表示满足 \(a_j \leq a_i\)\(b_j \leq b_i\)\(c_j \leq c_i\)\(j \neq i\) 的数的数量。主要在于求偏序个数。

复杂度分析

如果我们暴力做这个问题,显然可以for循环搞出来,复杂度为 \(O(n^2k)\)
显然对于 \(n = 1e5\) 的本意无法通过,而接下来我们 CDQ 分治时间复杂度就很快了,大概是一个 \(O(n \log^2 n)\) 的复杂度。

解题思路

首先对于这个问题,本质是是一个三维的比较问题,我们对于第一位进行排序,此时第一维就已经有序了 \(a_i \leq a_j\) 已保证,相当于降了一维,接下来是对于剩下两维的处理。

我们用分治来处理第二维,从 \([1, n]\) 开始逐层递归。
具体来说,对于子问题 \([l, r]\)

  • \(mid\)\(l, r\) 中点。
  • 递归解决 \([l, mid]\)\([mid + 1, r]\)
  • 统计 \([l, mid]\)\([mid + 1, r]\) 的贡献。

对于第三步直接双指针,令 \(i \in [l, mid], j \in [mid + 1, r]\)。目前我们还需要统计符合 \(b_i \leq b_j, c_i \leq c_j\) 的点对个数。
我们可以枚举 \(j\),看看有多少符合,于是在 \(j\) 指针统计贡献时,左区间即之前所有i计入的贡献已经满足了 \(a_j \leq a_i\)\(b_j \leq b_i\) 的条件,怎么统计 \(c_j \leq c_i\) 的个数呢?

需要单点修改与前缀查询,选择树状数组。可以在值域上开树状数组(BIT),将 \(c_i\) 压入 \(BIT\) ,查询 \(c_j\) 就直接求 \(sum[0 , c_j)\)

code

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

const int N = 2e5+10;

struct point{
    int a,b,c;
    int cnt;
    int ans; 
    point() : cnt(1),ans(0){};
}q[N],tmp[N];

bool operator == (const point &x,const point &y){
    return x.a==y.a && x.b==y.b && x.c==y.c;
}

int n,k;
int c[N];
int r_ans[N];

int lowbit(int x) {return x & -x;}
void adde(int pos,int val){
    while(pos<=k){
        c[pos]+=val;
        pos+=lowbit(pos);
    }
}
int query(int pos){
    int ret=0;
    while(pos){
        ret+=c[pos];
        pos-=lowbit(pos);
    }
    return ret;
}

void cdq(int l,int r){
    if(l>=r) return;
    int mid=(l+r)>>1;
    cdq(l,mid);cdq(mid+1,r);

    int i=l,j=mid+1,k=l;
    while(i<=mid && j<=r){
        if(q[i].b<=q[j].b){
            adde(q[i].c,q[i].cnt);
            tmp[k++]=q[i++];
        }else{
            q[j].ans+=query(q[j].c);
            tmp[k++]=q[j++];
        }
    }
    
    while(i<=mid){adde(q[i].c,q[i].cnt);tmp[k++]=q[i++];}
    while(j<=r){q[j].ans+=query(q[j].c);tmp[k++]=q[j++];}
    
    for(int i=l;i<=mid;i++) adde(q[i].c,-q[i].cnt);

    for(int i=l;i<=r;i++) q[i]=tmp[i];
    
    return;
}

int main(){
    cin.tie(0)->sync_with_stdio(0);
    cin>>n>>k;
    for(int i=1;i<=n;i++){
        cin>>q[i].a>>q[i].b>>q[i].c;
    }
    

    sort(q+1,q+n+1,[](const point &x,const point &y){
        if(x.a != y.a) return x.a < y.a;
        if(x.b != y.b) return x.b < y.b;
        return x.c < y.c;
    });

    int idx=0;
    for(int i=1;i<=n;i++){
        if(q[i]==q[i-1]){
            q[idx].cnt++;
        }else{
            q[++idx]=q[i];
        }
    }

    cdq(1,idx);

    for(int i=1;i<=idx;i++){
        r_ans[q[i].cnt+q[i].ans]+=q[i].cnt;
    }
    for(int i=1;i<=n;i++) cout<<r_ans[i]<<"\n";
    return 0;
}

笔者学习时疑问一

对于第二维第三步你可能会有一些疑问,我们本身第一维已经拍好序了为什么到了第二维反而要用双指针破坏第一维a的顺序呢,这就体现出来了 CDQ 分治的精妙之处,我们把一个大问题拆成了诸多小问题,会发现向下拆分的过程中并没有进行操作,在向上合并的过程中才有操作,我们只需要保证当前层是有序的就不会有影响,当前层的答案是无后效性的,如果你也有过与笔者同样的疑问,那请你停下来仔细思考这个地方是没有问题的。

笔者学习时疑问二

对于第二维的操作我们在合并排序时让左区间的加入树状数组,右区间的查询,笔者在想会不会漏掉一些情况。

  1. 左区间不查询是因为在左区间的子区间中已经统计过。
  2. 右区间能询问完全。分两种情况:a.合并时右区间的加入就会查询。b.没有用到的右区间是为满足所有第二位条件的,会在最后归并完全部查询。

笔者学习时疑问三

对于左区间如果有多的点为什么还要压入树状数组,而非直接跳过。

其实本质上就是直接跳过,他没有被统计,之后树状数组也会清空,多出的点跑完的价值在于填满tmp数组。

posted @ 2026-04-02 19:07  词印  阅读(52)  评论(2)    收藏  举报