cdq 分治学习笔记
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, r]\) 的和。
- 将答案减去 \([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 分治学习笔记的更多相关文章
- 初学cdq分治学习笔记(可能有第二次的学习笔记)
前言骚话 本人蒟蒻,一开始看到模板题就非常的懵逼,链接,学到后面就越来越清楚了. 吐槽,cdq,超短裙分治....(尴尬) 正片开始 思想 和普通的分治,还是分而治之,但是有一点不一样的是一般的分治在 ...
- CDQ分治学习笔记
数据结构中的一块内容:$CDQ$分治算法. $CDQ$显然是一个人的名字,陈丹琪(NOI2008金牌女选手) 这种离线分治算法被算法界称为"cdq分治" 我们知道,一个动态的问题一 ...
- [摸鱼]cdq分治 && 学习笔记
待我玩会游戏整理下思绪(分明是想摸鱼 cdq分治是一种用于降维和处理对不同子区间有贡献的离线分治算法 对于常见的操作查询题目而言,时间总是有序的,而cdq分治则是耗费\(O(logq)\)的代价使动态 ...
- CDQ分治学习笔记(三维偏序题解)
首先肯定是要膜拜CDQ大佬的. 题目背景 这是一道模板题 可以使用bitset,CDQ分治,K-DTree等方式解决. 题目描述 有 nn 个元素,第 ii 个元素有 a_iai.b_ibi.c_ ...
- 三维偏序[cdq分治学习笔记]
三维偏序 就是让第一维有序 然后归并+树状数组求两维 cdq+cdq不会 告辞 #include <bits/stdc++.h> // #define int long long #def ...
- CDQ分治学习思考
先挂上个大佬讲解,sunyutian1998学长给我推荐的mlystdcall大佬的[教程]简易CDQ分治教程&学习笔记 还有个B站小姐姐讲解的概念https://www.bilibili.c ...
- cdq分治学习
看了stdcall大佬的博客 传送门: http://www.cnblogs.com/mlystdcall/p/6219421.html 感觉cdq分治似乎很多时候都要用到归并的思想
- [Updating]点分治学习笔记
Upd \(2020/2/15\),又补了一题 LuoguP2664 树上游戏 \(2020/2/14\),补了一道例题 LuoguP3085 [USACO13OPEN]阴和阳Yin and Yang ...
- 点分治&&动态点分治学习笔记
突然发现网上关于点分和动态点分的教程好像很少……蒟蒻开篇blog记录一下吧……因为这是个大傻逼,可能有很多地方写错,欢迎在下面提出 参考文献:https://www.cnblogs.com/LadyL ...
- 学习笔记 | CDQ分治
目录 前言 啥是CDQ啊(它的基本思想) 例题 后记 参考博文 前言 博主太菜了 学习快一年的OI了 好像没有什么会的算法 更寒碜的是 学一样还不精一样TAT 如有什么错误请各位路过的大佬指出啊感谢! ...
随机推荐
- DLR 的扩展库 Dynamitey
.NET 在 CLR 对动态语言或者脚本语言的支持是通过DLR 完成的, Miguel de Icaza对 DLR 的特点概括如下: 一个针对动态语言的共享式类型系统: 一个共享的 AST,可以被语言 ...
- [Kogel.Subscribe.Mssql]SQL Server增量订阅,数据库变更监听
此框架是SQL Server增量订阅,用来监听增删改数据库数据变更 目前仅支持SQL Server,Nuget上可以下载安装 或者使用Nuget命令添加包 dotnet add package Kog ...
- 【SQL进阶】【REPLACE/TIMESTAMPDIFF/TRUNCATE】Day01:增删改操作
一.插入记录 1.插入多条记录 自己的答案: INSERT INTO exam_record(uid, exam_id, start_time, submit_time, score) VALUES ...
- Java面试多线程/JUC等知识
2021年6月30日完成此部分内容整理
- python random模块几个常用方法
python random模块几个常用方法 random.random()方法 random.uniform(a, b)方法 random.randint(a, b)方法 random.randran ...
- O-MVLL代码混淆方式
在介绍O-MVLL之前,首先介绍什么是代码混淆以及基于LLVM的代码混淆,O-MVLL项目正是基于此而开发来的. 有关O-MVLL的概括介绍以及安装和基本使用方式,可参见另一篇随笔 https://w ...
- 0停机迁移Nacos?Java字节码技术来帮忙
摘要:本文介绍如何将Spring Cloud应用从开源Consul无缝迁移至华为云Nacos. 本文分享自华为云社区<0停机迁移Nacos?Java字节码技术来帮忙>,作者:华为云PaaS ...
- Windows 安装 Zookeeper 详细步骤
Windows 安装 Zookeeper 详细步骤 一. Zookeeper 安装包下载 1.官网地址 Zookeeper官网 2.安装包下载 这里选择目前的稳定版 3.6.3 下载 可以看到有两个选 ...
- kali之pip和pip3安装
安装pip 首先安装setuptools,setuptools是 Python Enterprise Application Kit(PEAK)的一个副项目,它 是一组Python的 distutil ...
- [编程基础] Python字符串替换笔记
Python字符串替换笔记 Python字符串替换笔记主要展示了如何在Python中替换字符串.Python中有以下几种替换字符串的方法,本文主要介绍前三种. replace方法(常用) transl ...