数据结构专题-学习笔记:cdq 分治
1. 前言
cdq 分治,是一种用于计算偏序问题的离线算法,常数较小,跑的肯定比 kdtree 要快。
如无特殊说明,默认下文的点不重合,数字都是正整数。
2. 详解
cdq 最经典的是解决三维偏序,四维偏序可以 cdq 套 cdq 但是五维以上偏序还是写 kdtree 得了。
一维偏序:一维坐标系 个点,第 个点坐标 ,对所有 求多少个 满足 。
显然直接排序即可。
二位偏序:二维坐标系 个点,第 个点坐标 ,对所有 求多少个 满足 。
注意到出现了两维影响,因此考虑先排序消除第一维影响,然后对第二维统计。
首先一遍排序按照第一维升序排序,然后对第二维归并排序。
设当前合并 区间,此时因为第一维已经排序了, 里面最大 小于 最小 ,现在要计算 对 区间的贡献,具体做法就是从 中取出一个数时看一眼 已经被取出了几个数,然后直接加入 中取出的数的答案即可。
三维偏序:三维坐标系 个点,第 个点坐标 ,对所有 求多少个 满足 。
还是对第一维排序,之后对第二维归并排序,不同的是现在有第三维的影响,因此考虑用树状数组。
从 取出时,树状数组上 对应的位置加上 1,从 中取出时,树状数组上对应的区间 中查询和即可,这就是 对这个数的贡献。
四维偏序:四维坐标系 个点,第 个点坐标 ,对所有 求多少 满足 。
对第一维排序,然后对第二维和第三维均作归并排序,第四维用树状数组。
此时你有两种做法,第一种是中序遍历版 cdq,第二种是后序遍历版 cdq。
中序遍历版:对于第二维的归并排序 ,此时我们先排 ,然后开始对第三维做 内的归并排序,仿照上面因为前面区间 都小于后面区间 所以第四维可以树状数组,第三维整个做完之后再做第二维上的 的归并排序。
后序遍历版就是将上面第二维 全部排完之后再做第三维 排序。
对于题设而言,使用后序遍历版也可以,但是如果涉及到一些奇怪的 dp,那么就必须使用中序遍历版,因为 dp 过程中我们需要保证前面的所有答案都已经被计算正确,比如说如果四维偏序下我们要做 dp (四维偏序下 ),要保证 dp 正确则必须采用中序遍历版。
GitHub:CodeBase-of-Plozia
Code(用了线段树代替树状数组):
/*
========= Plozia =========
Author:Plozia
Problem:P3810 【模板】三维偏序(陌上花开)
Date:2022/4/5
========= Plozia =========
*/
#include <bits/stdc++.h>
typedef long long LL;
const int MAXN = 1e5 + 5, MAXK = 2e5 + 5;
int n, m, f[MAXN], s[MAXN], cntn, val[MAXN];
struct node { int a, b, c, id; } a[MAXN], b[MAXN], tmp[MAXN];
struct sgt { int sum; } tree[MAXK << 2];
int Read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = sum * 10 + (ch ^ 48);
return sum * fh;
}
int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }
bool cmp(const node &fir, const node &sec)
{
if (fir.a ^ sec.a) return fir.a < sec.a;
if (fir.b ^ sec.b) return fir.b < sec.b;
return fir.c < sec.c;
}
void Change(int p, int k, int s, int lp, int rp)
{
if (lp == rp) { tree[p].sum += s; return ; }
int mid = (lp + rp) >> 1;
if (k <= mid) Change(p << 1, k, s, lp, mid);
else Change(p << 1 | 1, k, s, mid + 1, rp);
tree[p].sum = tree[p << 1].sum + tree[p << 1 | 1].sum;
}
int Ask(int p, int l, int r, int lp, int rp)
{
if (lp >= l && rp <= r) return tree[p].sum;
int mid = (lp + rp) >> 1, val = 0;
if (l <= mid) val += Ask(p << 1, l, r, lp, mid);
if (r > mid) val += Ask(p << 1 | 1, l, r, mid + 1, rp);
return val;
}
void MergeSort(int l, int r)
{
if (l == r) return ; int mid = (l + r) >> 1;
MergeSort(l, mid); MergeSort(mid + 1, r);
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r)
{
if (a[i].b <= a[j].b) { Change(1, a[i].c, val[a[i].id], 1, m); tmp[k++] = a[i++]; }
else { f[a[j].id] += Ask(1, 1, a[j].c, 1, m); tmp[k++] = a[j++]; }
}
while (j <= r) { f[a[j].id] += Ask(1, 1, a[j].c, 1, m); tmp[k++] = a[j++]; }
for (int p = l; p < i; ++p) Change(1, a[p].c, -val[a[p].id], 1, m);
while (i <= mid) tmp[k++] = a[i++];
for (int i = l; i <= r; ++i) a[i] = tmp[i];
}
int main()
{
n = Read(), m = Read();
for (int i = 1; i <= n; ++i) b[i].a = Read(), b[i].b = Read(), b[i].c = Read();
std::sort(b + 1, b + n + 1, cmp);
for (int i = 1; i <= n; ++i)
{
if (b[i].a != b[i - 1].a || b[i].b != b[i - 1].b || b[i].c != b[i - 1].c)
{
++cntn; a[cntn] = b[i]; val[cntn] = 1; a[cntn].id = cntn;
}
else ++val[cntn];
}
MergeSort(1, cntn);
for (int i = 1; i <= cntn; ++i) s[f[i] + val[i] - 1] += val[i];
for (int i = 0; i < n; ++i) printf("%d\n", s[i]);
return 0;
}
3. 总结
cdq 分治:首先对第一维排序,然后中间维通过归并排序消除影响,注意先排 然后对下一维做 归并排序最后在这一维排 ,倒数第二维正常排序,最后一维树状数组。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具