CDQ 分治是一种很玄学的东西。


Part 0 引子

相信大家都会归并排序,又都知道归并排序求逆序对怎么求的

如果连归并求逆序对都不会那你为什么要学 cdq 分治阿喂

void merge_sort(int l, int r) {
if(l >= r) return ;
int mid = (l + r) >> 1;
merge_sort(l, mid);
merge_sort(mid + 1, r);
int L = l, R = mid + 1, k = 0;
while(L <= mid && R <= r)
if(a[L] <= a[R]) tmp[k++] = a[L++];
else tmp[k++] = a[R++], ans += mid - L + 1;
while(L <= mid) tmp[k++] = a[L++];
while(R <= r) tmp[k++] = a[R++];
for(int p = l, i = 0; p <= r; p++, i++)
a[p] = tmp[i];
}

tnnd,为什么不用树状数组!为什么不用!

事实上这也是 CDQ 分治,让我们来思考思考它是如何工作的:

  • 排序的区间先处理 \([l, mid]\)。
  • 处理 \([mid + 1, r]\)。
  • 统计。

我们来重点思考一下,这个统计是怎么做的——

  • 这个统计的大前提,是我们的 \(a_l\sim a_{mid}\) 和 \(a_{mid} + 1, a_r\) 都已经有序,没有这个大前提,就没有后面的合并。
  • 尝试合并这两个有序的序列。
  • 在合并过程中,我们都知道逆序对定义如 \(a_i < a_j (j > i)\)。我们如果发现在合并的时候有个 \(a_L > a_R\),这说明 \(a_L\sim a_{mid}\) 必然都大于 \(a_R\),故逆序对数量加上 \(mid - L + 1\)。
  • 总结一下,本质上就是看统计两个区间对答案能造成多少贡献。

总结到这里,我们就可以得到一个大致的 CDQ 分治的流程了——

  • 先处理 \([l, mid]\)。
  • 再处理 \([mid + 1, r]\)。
  • 统计两个区间对答案造成的贡献。

    还不理解?下面我们通过几道例题来更深入的理解 CDQ 分治。

Part 1 CDQ 分治维护静态问题与偏序问题

CDQ 分治可以维护静态问题,尤其是偏序问题。

我们上面提到的逆序对实际上就是一维偏序。

二维偏序与一维偏序类似,就是变成了二维,如果 \(a_i \le a_j, b_i \le b_j\) 则称之为一个偏序。

我们可以以 \(a_i\) 为第一关键字,\(b_i\) 为第二关键字降序排序,这样的话就消除了 \(a_i\) 的影响,现在只要考虑 \(b_i\) 的影响了。我们按照排序过的 \(a_i\) 的顺序挨个把 \(b_i\) 加入树状数组,那么目前就能统计出 \(\operatorname{query}(b_i)\) 个偏序。

有了二维偏序的基础,我们再来看看三维偏序 link

联系上面二维偏序的方法,我们容易想到,我们先以 \(a_i\) 为第一关键字排个序,排序完了再进行 CDQ 分治。下面就变成了一个只剩下两维的偏序问题,做法其实和上文的差不多。

先处理 \([l, mid]\),再处理 \([mid + 1, r]\)。然后是 CDQ 分治的重头戏:统计影响。

  • 我们还是为了消维度影响,我们考虑对 \([l, mid]\) 和 \([mid + 1, r]\) 这两个区间分别再以 \(b_i\) 为第一关键字降序排序,这样也消除了 \(b\) 的影响。
  • 下面建立两个指针,\(p\) 指向 \([mid + 1, r]\),\(pos\) 指向 \([l, mid]\)。
  • 不断枚举 \(p\) 的位置,同时控制 \(pos\) 的位置。不断尝试右移 \(pos\)。当 \(b_p \le b_{pos}\) 且 \(pos\) 不越过区间(即小于等于 \(mid\) )时可以消除 \(a\) 与 \(b\) 的影响。因为 \(pos\) 始终小于等于 \(p\),故 \(a_{pos}\) 始终小于等于 \(a_p\)。既然 \(a_{pos} \ge a_p, b_{pos} \ge a_p\),我们只要康康 \(c_{pos}\) 的影响即可。
  • 建立一个树状数组维护前缀和,我们在 \(c_{pos}\) 位置添加这个三元组出现的次数。
  • 结束了这个流程,最后对答案造成的贡献是树状数组内 \(c_p\) 的前缀和(因为大于 \(c_p\) 的 \(c\) 是不能用滴。)
#include <iostream>
#include <algorithm>
#define MAXN 100000
#define QWQ cout << "qwq" << endl;
using namespace std;
struct node {
int a, b, c, cnt, ans;
} I[MAXN + 10], A[MAXN + 10];
int S = 0, n, k;
int ANS[MAXN + 10];
//-----
int tr[MAXN * 4 + 10];
int lowbit(int x) {return (x & (-x));}
void updata(int x, int y) {
while(x <= k) {
tr[x] += y;
x += lowbit(x);
}
}
int ask(int x) {
int sum = 0;
while(x) {
sum += tr[x];
x -= lowbit(x);
}
return sum;
}
//-----
bool cmp1(node &x, node &y) {
if(x.a == y.a) if(x.b != y.b) return x.b < y.b; else return x.c < y.c;
else return x.a < y.a;
}
bool cmp2(node &x, node &y) {
if(x.b == y.b) return x.c < y.c;
else return x.b < y.b;
}
void stO_cdq_Orz(int l, int r) {//即上文所讲的 cdq 分治
if(l == r) return ;
int mid = (l + r) >> 1;
stO_cdq_Orz(l, mid);
stO_cdq_Orz(mid + 1, r);
sort(A + l, A + mid + 1, cmp2);
sort(A + mid + 1, A + r + 1, cmp2);
int pos = l;
for(int p = mid + 1; p <= r; p++) {//统计影响的代码
while(A[p].b >= A[pos].b && pos <= mid) {
updata(A[pos].c, A[pos].cnt);
pos++;
}
A[p].ans += ask(A[p].c);
}
for(int p = l; p < pos; p++)//记得清空树状数组 \ovo/
updata(A[p].c, -A[p].cnt);
}
int main() {
cin >> n >> k;
for(int p = 1; p <= n; p++)
cin >> I[p].a >> I[p].b >> I[p].c;
sort(I + 1, I + n + 1, cmp1);
int qwq = 0;
for(int p = 1; p <= n; p++) {//事实上这个过程实在去重
qwq++;
if(I[p].a != I[p + 1].a || I[p].b != I[p + 1].b || I[p].c != I[p + 1].c)
A[++S].a = I[p].a, A[S].b = I[p].b, A[S].c = I[p].c, A[S].cnt = qwq, qwq = 0;
}
stO_cdq_Orz(1, S);//cdq 太强啦!
for(int p = 1; p <= S; p++)
ANS[A[p].ans + A[p].cnt - 1] += A[p].cnt;
//解释一下为什么要这样统计答案,是个很小得细节。
//A[p].ans 代表的是带这个三元组的偏序数量,加上 A[p].cnt 是因为 (a,b,c) 与 (a,b,c) 也是偏序,减一是因为这里面包含了 p 号三元组与 p 号三元组本身的偏序,重复了
//为什么在加上 A[p].cnt 时不减一?因为本来就是缺了 A[p].cnt 个偏序,这里就大家都没有差别了
//如果大家还不懂的话可以自己琢磨琢磨,很简单的
for(int p = 0; p < n; p++) cout << ANS[p] << endl;
}

Part 2 CDQ 分治维护动态问题

这里会谈 CDQ 分治是如何维护动态问题的。

Part 2.1 维护树状数组

tnnd,为什么不用树状数组!为什么不用!

CDQ 分治也可以用来维护树状数组基本题 link

我们都会树状数组,我们都知道树状数组其实是在维护前缀和,也就是说我们可以把这个问题分解成三个操作:

  1. 单点修改。
  2. 将答案加上 \([1, r]\) 的和。
  3. 将答案减去 \([1, l - 1]\) 的和。

我们就很轻松的可以整出来这几个元素:

  • \(opt_i\), 代表操作的编号。
  • \(val_i\),仅对于 \(opt_i = 1\) 的情况,是要修改的值。
  • \(pos_i\),仅对于 \(opt_i = 2\) 或 \(3\),即修改位置。

还是老规矩,cdq 分治。有如下几个步骤:

  • 处理 \([l, mid]\)。
  • 处理 \([mid + 1, t]\)。
  • 统计。我们要统计答案,也就是说我们要统计左边对右边产生的影响,既然是影响,那么左半边我们要统计一波修改(即 \(opt_i = 1\)),右半边统计的是查询(\(opt_i = 2\) 或 \(3\))。
  • 对于左半边的修改,我们可以直接用一个变量统计这些修改所改动的值 \(res\),对于右半边的查询,我们可以直接对其进行增减。

不过,cdq 分治有个大前提,那就是 \([l,mid]\) 和 \([mid + 1, r]\) 是有序的。如果无序,那么又何谈影响?

下面我们就要来讨论讨论关于顺序的问题了。根据我们上面的叙述,事实上对于 \(opt_i = 1\) 来说,它的操作必然会对 \([mid + 1, r]\) 里面的查询造成影响,也就是说这个位置肯定要在右半边查询的位置之内(不然的话不就查不到了吗)。

所以,操作的位置是我们排序的第一关键字,同时对于操作位置相同的操作,以操作的编号为第二关键字(显然修改要放在查询前面做),降序排序,保证 \([l, mid]\) 的修改位置一定不超过 \([mid + 1, r]\) 的查询位置。

不过我们可以直接在 cdq 分治内部用归并做这个排序。

#include <iostream>
#define MAXN 500000
#define QWQ cout << "QWQ" << endl;
using namespace std;
struct node {
int opt, pos, val, id;
bool operator < (const node &other) const {
if(pos != other.pos) return pos < other.pos;
else return opt < other.opt;
}
} a[MAXN * 2 + 10], tmp[MAXN * 2 + 10];
int ans[MAXN + 10];
void sto_cdq_orz(int l, int r) {
if(l >= r) return ;
int mid = (l + r) >> 1;
sto_cdq_orz(l, mid);
sto_cdq_orz(mid + 1, r);
int p = mid + 1, pos = l, rest = 0, c = l - 1;
while(pos <= mid && p <= r) {
if(a[pos] < a[p]) {
if(a[pos].opt == 1) rest += a[pos].val;
tmp[++c] = a[pos++];
}
else {
if(a[p].opt == 2) ans[a[p].id] += rest;
else if(a[p].opt == 3) ans[a[p].id] -= rest;
tmp[++c] = a[p++];//一个归并的过程
}
}
while(pos <= mid) tmp[++c] = a[pos++];//因为这时候右半边都处理完了,没有查询了,所以不需要判断操作
while(p <= r) {//右半边还没有完全处理完毕
if(a[p].opt == 2) ans[a[p].id] += rest;
else if(a[p].opt == 3) ans[a[p].id] -= rest;
tmp[++c] = a[p++];
}
for(p = l; p <= r; p++) a[p] = tmp[p];
}
int main() {
int n, m, qwq = 0, qaq = 0;
cin >> n >> m;
for(int p = 1, x; p <= n; p++) {
cin >> x;
a[++qwq].opt = 1, a[qwq].pos = p;
a[qwq].val = x, a[qwq].id = 0;
}
for(int p = 1; p <= m; p++) {
int opt, x, y;
cin >> opt >> x >> y;
if(opt == 1) {
a[++qwq].opt = 1, a[qwq].pos = x;
a[qwq].val = y, a[qwq].id = 0;
}
else {
a[++qwq].opt = 2, a[qwq].pos = y, a[qwq].val = -114514, a[qwq].id = ++qaq;//id 是第几个查询,为了输出方便的
a[++qwq].opt = 3, a[qwq].pos = x - 1, a[qwq].val = -11, a[qwq].id = qaq;
}
}
sto_cdq_orz(1, qwq);
for(int p = 1; p <= qaq; p++)
cout << ans[p] << endl;
}

再来看一道题天使玩偶,很经典的 cdq 分治。

概括题意为:

\(n + m\) 个操作,操作有添加一个点和查询目前点集里与其曼哈顿距离最小的点。

姑且不看添加操作,先看查询操作。

曼哈顿距离是有绝对值的式子,\(|x - x'| + |y - y'|\),但是这个式子很难搞,我们要化简一下,以 \(x' \le x, y' \le y\) 为例,我们要求的距离就转化成了 \(\min\{x - x' + y - y'\}\)。

提取已知条件转化式子是基操。\(x, y\) 已知,弄出来得到 \(x + y - \max\{x' + y'\}\)。

问题转化成,如何维护 \(\max\{x', y'\}\) 同时保证 \(x' \le x, y' \le y\)。

等等,\(x' \le x, y'\le y\)?这不是二维偏序的式子吗!

那么方案就呼之欲出啦!把初始坐标和查询坐标按照横坐标降序排序,用一个树状数组维护。树状数组维护 \(y'\) 位置的 \(x' + y'\) 最大值。查询时,答案就是 \(\min\{x - y - \operatorname{query}(y)\}\)。因为排序满足 \(x'\le x\),树状数组满足 \(y'\le y\)。

下面,我们要考虑带修改的操作了!

其实这里的统计和上面树状数组的统计一个套路。左半边有修改,左半边进行修改。右半边有查询,右半边进行查询。

最后我们要处理答案了。首先我们上面处理的是左下角的坐标,而不是全局的坐标。你可以多推几个式子,但是事实上反转一下整个坐标系就可以了。

代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#define MAXN 1000000
#define QWQ cout << "QWQ" << endl;
using namespace std;
int f = 0;
int INF;
struct node {
int opt, x, y, id;
} a[MAXN + 10];
//qaq
int tr[MAXN + 10];
int lowbit(int x) {
return x & (-x);
}
int n;
void change(int x, int y) {
for(int p = y; p <= 1000000; p += lowbit(p))
tr[p] = max(tr[p], x + y);
}
int query(int x) {
int res = 0;
while(x) {
res = max(res, tr[x]);
x -= lowbit(x);
}
return res;
}
void clear(int pos) {
for(; pos <= 1000000; pos += lowbit(pos))
tr[pos] = 0;
}
//qwq
node tmp[MAXN + 10];
int ans[MAXN + 10];
void sto_cdq_orz(int l, int r) {
if(l >= r) return ;
int mid = (l + r) >> 1;
sto_cdq_orz(l, mid);
sto_cdq_orz(mid + 1, r);
int pos = l, p = mid + 1, c = l - 1;
while(pos <= mid && p <= r) {
if(a[pos].x <= a[p].x) {//修改
if(a[pos].opt == 1) change(a[pos].x, a[pos].y);
tmp[++c] = a[pos], pos++;
}
else {
if(a[p].opt == 2) {//统计
int qaq = query(a[p].y);
if(qaq) ans[a[p].id] = min(ans[a[p].id], a[p].x + a[p].y - qaq);
}
tmp[++c] = a[p], p++;
}
}
while(pos <= mid) tmp[++c] = a[pos], pos++;
while(p <= r) {
if(a[p].opt == 2) {//统计
int qaq = query(a[p].y);
if(qaq) ans[a[p].id] = min(ans[a[p].id], a[p].x + a[p].y - qaq);
}
tmp[++c] = a[p], p++;
}
for(int p = l; p <= r; p++) a[p] = tmp[p];
for(int p = l; p <= r; p++)
if(a[p].opt == 1) clear(a[p].y);
return ;
}
//
node qwq[MAXN + 10];
void init() {
int m;
cin >> n >> m;
for(int p = 1, x, y; p <= n; p++) {
cin >> x >> y;
x++, y++;//为什么要加一?因为如果出现 0 的坐标,在树状数组里就会死循环
a[p].opt = 1, a[p].x = x, a[p].y = y;
}
for(int p = 1, opt, x, y; p <= m; p++) {
cin >> a[++n].opt >> a[n].x >> a[n].y;
a[n].x++, a[n].y++;
if(a[n].opt == 2) f++, a[n].id = f;
}
for(int p = 1; p <= n; p++)
qwq[p] = a[p];
}
void work() {
memset(ans, 0x7f, sizeof(ans));
memset(ans, 0x3f, sizeof(ans));
sto_cdq_orz(1, n);
for(int p = 1; p <= n; p++) a[p] = qwq[p], a[p].x = 1000001 - a[p].x;//反转
sto_cdq_orz(1, n);
for(int p = 1; p <= n; p++) a[p] = qwq[p], a[p].y = 1000001 - a[p].y;
sto_cdq_orz(1, n);
for(int p = 1; p <= n; p++) a[p] = qwq[p], a[p].x = 1000001 - a[p].x, a[p].y = 1000001 - a[p].y;
sto_cdq_orz(1, n);
}
int main() {
init();
work();
for(int p = 1; p <= f; p++)
cout << ans[p] << endl;
}

为什么这道题我们采用 cdq 分治?因为这样的话就避免了各种高级数据结构,好写,好调。不然的话可能要用树套树之类的东西维护前驱后继,这样就麻烦许多,调试起来很繁琐。

Part 3 总结

cdq 分治真是好呢!

cdq 分治学习笔记的更多相关文章

  1. 初学cdq分治学习笔记(可能有第二次的学习笔记)

    前言骚话 本人蒟蒻,一开始看到模板题就非常的懵逼,链接,学到后面就越来越清楚了. 吐槽,cdq,超短裙分治....(尴尬) 正片开始 思想 和普通的分治,还是分而治之,但是有一点不一样的是一般的分治在 ...

  2. CDQ分治学习笔记

    数据结构中的一块内容:$CDQ$分治算法. $CDQ$显然是一个人的名字,陈丹琪(NOI2008金牌女选手) 这种离线分治算法被算法界称为"cdq分治" 我们知道,一个动态的问题一 ...

  3. [摸鱼]cdq分治 && 学习笔记

    待我玩会游戏整理下思绪(分明是想摸鱼 cdq分治是一种用于降维和处理对不同子区间有贡献的离线分治算法 对于常见的操作查询题目而言,时间总是有序的,而cdq分治则是耗费\(O(logq)\)的代价使动态 ...

  4. CDQ分治学习笔记(三维偏序题解)

    首先肯定是要膜拜CDQ大佬的. 题目背景 这是一道模板题 可以使用bitset,CDQ分治,K-DTree等方式解决. 题目描述 有 nn 个元素,第 ii 个元素有 a_iai​.b_ibi​.c_ ...

  5. 三维偏序[cdq分治学习笔记]

    三维偏序 就是让第一维有序 然后归并+树状数组求两维 cdq+cdq不会 告辞 #include <bits/stdc++.h> // #define int long long #def ...

  6. CDQ分治学习思考

    先挂上个大佬讲解,sunyutian1998学长给我推荐的mlystdcall大佬的[教程]简易CDQ分治教程&学习笔记 还有个B站小姐姐讲解的概念https://www.bilibili.c ...

  7. cdq分治学习

    看了stdcall大佬的博客 传送门: http://www.cnblogs.com/mlystdcall/p/6219421.html 感觉cdq分治似乎很多时候都要用到归并的思想

  8. [Updating]点分治学习笔记

    Upd \(2020/2/15\),又补了一题 LuoguP2664 树上游戏 \(2020/2/14\),补了一道例题 LuoguP3085 [USACO13OPEN]阴和阳Yin and Yang ...

  9. 点分治&&动态点分治学习笔记

    突然发现网上关于点分和动态点分的教程好像很少……蒟蒻开篇blog记录一下吧……因为这是个大傻逼,可能有很多地方写错,欢迎在下面提出 参考文献:https://www.cnblogs.com/LadyL ...

  10. 学习笔记 | CDQ分治

    目录 前言 啥是CDQ啊(它的基本思想) 例题 后记 参考博文 前言 博主太菜了 学习快一年的OI了 好像没有什么会的算法 更寒碜的是 学一样还不精一样TAT 如有什么错误请各位路过的大佬指出啊感谢! ...

随机推荐

  1. DLR 的扩展库 Dynamitey

    .NET 在 CLR 对动态语言或者脚本语言的支持是通过DLR 完成的, Miguel de Icaza对 DLR 的特点概括如下: 一个针对动态语言的共享式类型系统: 一个共享的 AST,可以被语言 ...

  2. [Kogel.Subscribe.Mssql]SQL Server增量订阅,数据库变更监听

    此框架是SQL Server增量订阅,用来监听增删改数据库数据变更 目前仅支持SQL Server,Nuget上可以下载安装 或者使用Nuget命令添加包 dotnet add package Kog ...

  3. 【SQL进阶】【REPLACE/TIMESTAMPDIFF/TRUNCATE】Day01:增删改操作

    一.插入记录 1.插入多条记录 自己的答案: INSERT INTO exam_record(uid, exam_id, start_time, submit_time, score) VALUES ...

  4. Java面试多线程/JUC等知识

    2021年6月30日完成此部分内容整理

  5. python random模块几个常用方法

    python random模块几个常用方法 random.random()方法 random.uniform(a, b)方法 random.randint(a, b)方法 random.randran ...

  6. O-MVLL代码混淆方式

    在介绍O-MVLL之前,首先介绍什么是代码混淆以及基于LLVM的代码混淆,O-MVLL项目正是基于此而开发来的. 有关O-MVLL的概括介绍以及安装和基本使用方式,可参见另一篇随笔 https://w ...

  7. 0停机迁移Nacos?Java字节码技术来帮忙

    摘要:本文介绍如何将Spring Cloud应用从开源Consul无缝迁移至华为云Nacos. 本文分享自华为云社区<0停机迁移Nacos?Java字节码技术来帮忙>,作者:华为云PaaS ...

  8. Windows 安装 Zookeeper 详细步骤

    Windows 安装 Zookeeper 详细步骤 一. Zookeeper 安装包下载 1.官网地址 Zookeeper官网 2.安装包下载 这里选择目前的稳定版 3.6.3 下载 可以看到有两个选 ...

  9. kali之pip和pip3安装

    安装pip 首先安装setuptools,setuptools是 Python Enterprise Application Kit(PEAK)的一个副项目,它 是一组Python的 distutil ...

  10. [编程基础] Python字符串替换笔记

    Python字符串替换笔记 Python字符串替换笔记主要展示了如何在Python中替换字符串.Python中有以下几种替换字符串的方法,本文主要介绍前三种. replace方法(常用) transl ...