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. SSH(五)spring整合hibernate

    一.创建hibernate实体映射文件. 在实体所在包创建映射文件product.hbm.xml,引入hibernate的映射约束.(该约束位于hibernate3.jar里面hibernate-ma ...

  2. 谁说.NET没有GC调优?只改一行代码就让程序不再占用内存

    经常看到有群友调侃"为什么搞Java的总在学习JVM调优?那是因为Java烂!我们.NET就不需要搞这些!"真的是这样吗?今天我就用一个案例来分析一下. 昨天,一位学生问了我一个问 ...

  3. MybatisPlus多表连接查询一对多分页查询数据

    一.序言 在日常一线开发过程中,多表连接查询不可或缺,基于MybatisPlus多表连接查询究竟该如何实现,本文将带你找到答案. 在多表连接查询中,既有查询单条记录的情况,又有列表查询,还有分页查询, ...

  4. python之yaml文件读取封装

    import os import yaml from yamlinclude import YamlIncludeConstructor YamlIncludeConstructor.add_to_l ...

  5. 【机器学习】李宏毅——Adversarial Attack(对抗攻击)

    研究这个方向的动机,是因为在将神经网络模型应用于实际场景时,它仅仅拥有较高的正确率是不够的,例如在异常检测中.垃圾邮件分类等等场景,那些负类样本也会想尽办法来"欺骗"模型,使模型无 ...

  6. 1+X Web初级笔记查漏补缺+练习题

    学习笔记: position:relative是相对定位,是相对于自身位置移动,但是占据原有空间 absolute是绝对定位,原有空间不保留会被其他元素挤占 绝对定位 absolute不占位置完全浮动 ...

  7. Potree 001 Potree介绍

    1.Potree是什么 Potree是一种基于WebGL的点云数据可视化解决方案,包含点云数据转化,以及进行可视化的源码.该解决方案的主要优势在于对点云数据进行了多尺度的管理,在数据传输和可视化上都做 ...

  8. C语言指针常见问题

    我们在学C语言时,指针是我们最头疼的问题之一,针对C语言指针,博主根据自己的实际学到的知识以及开发经验,总结了以下使用C语言指针时常见问题. 指针 指针做函数参数 学习函数的时候,讲了函数的参数都是值 ...

  9. 【大型软件开发】浅谈大型Qt软件开发(一)开发前的准备——在着手开发之前,我们要做些什么?

    前言 最近我们项目部的核心产品正在进行重构,然后又是年底了,除了开发工作之外项目并不紧急,加上加班时间混不够了....所以就忙里偷闲把整个项目的开发思路聊一下,以供参考. 鉴于接下来的一年我要操刀这个 ...

  10. 可持久化栈学习笔记 | 题解 P6182 [USACO10OPEN]Time Travel S

    简要题意 你需要维护一个栈,有 \(n\) 个操作,支持: 给定一个 \(x\),将 \(x\) 加入栈. 将一个元素出栈. 给定一个 \(x\),将当前栈回退到 第 \(x\) 操作前. 每一次操作 ...