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. java.util.Date和java.util.Calendar

    Date date = new Date();//分配初始化一个Date()对象 Calendar cal = Calendar.getInstance();//获取一个基于当前时间的日历 int d ...

  2. 【PostgreSQL】PG通过SQL语句读取二进制bytea类型并进行二进制和十六进制转换

    1.将二进制编码为十六进制 select encode("AUUID_0",'hex'),"AUUID_0" from wxf_test."ABANK ...

  3. pandas中loc和iloc的使用细节

    1.缘由 前段时间在使用pandas库中的索引和切片的时候,突然就感觉有点懵,赋值和索引的操作总是报错. 网上的很多资料讲的也非常的浅显,而且使用起来非常不顺手. 于是我就找到很多的网上资料,然后自己 ...

  4. 下载Font Awesome框架

    目录 一:下载Font Awesome框架 二:如何使用font awesome 1.使用图标等样式,点击复制标签即可,需要嵌套在i标签内 2.点击图标,复制标签,然后粘贴使用即可. 3.动态图片等 ...

  5. 《吐血整理》高级系列教程-吃透Fiddler抓包教程(36)-掌握Fiddler中Fiddler Script用法,你会有多牛逼-上篇

    1.简介 Fiddler是一款强大的HTTP抓包工具,它能记录所有客户端和服务器的http和https请求,允许你监视,设置断点,甚至修改输入输出数据. 使用Fiddler无论对开发还是测试来说,都有 ...

  6. 【JVM故障问题排查心得】「内存诊断系列」Docker容器经常被kill掉,k8s中该节点的pod也被驱赶,怎么分析?

    背景介绍 最近的docker容器经常被kill掉,k8s中该节点的pod也被驱赶. 我有一个在主机中运行的Docker容器(也有在同一主机中运行的其他容器).该Docker容器中的应用程序将会计算数据 ...

  7. Git强制覆盖master

    场景 由于公司的项目中,有一个开发分支(这里假设dev​)是一个严重偏离master​,需要我去强制覆盖master​. 问题 这个场景带来了两个问题: ​master​是受保护不能强推 ​dev​分 ...

  8. P8881 懂事时理解原神

    简要题意 \(T\) 组数据,每组数据给出一个 \(n\) 个顶点,\(m\) 条边的无向无权图.求出使用下面的伪代码求 \(1\) 为源点的单源最短路答案正确的概率.保留 \(3\) 位小数. in ...

  9. MySQL 插入数据 数据重复 从另一个表导入数据

    当使用MySQL插入数据时,我们可以根据需求选择合适的插入语句. 一.方法分类 二.具体方法 使用场景 作用 语句 注意 常规插入 忽略字段名 insert into 表名 values (值1, 值 ...

  10. angular8实现前端携带cookie发送给后端+nodejs获取前端发送的cookie

    1.前端测试代码angular8 // 测试代码 testCookie() { const url = 'http://10.11.11.11:3000/test/cookie' const para ...