数据结构专题-学习笔记:cdq 分治
1. 前言
cdq 分治,是一种用于计算偏序问题的离线算法,常数较小,跑的肯定比 kdtree 要快。
如无特殊说明,默认下文的点不重合,数字都是正整数。
2. 详解
cdq 最经典的是解决三维偏序,四维偏序可以 cdq 套 cdq 但是五维以上偏序还是写 kdtree 得了。
一维偏序:一维坐标系 \(n\) 个点,第 \(i\) 个点坐标 \(a_i\),对所有 \(i\) 求多少个 \(j\) 满足 \(a_j<a_i\)。
显然直接排序即可。
二位偏序:二维坐标系 \(n\) 个点,第 \(i\) 个点坐标 \((a_i,b_i)\),对所有 \(i\) 求多少个 \(j\) 满足 \(a_j<a_i,b_j<b_i\)。
注意到出现了两维影响,因此考虑先排序消除第一维影响,然后对第二维统计。
首先一遍排序按照第一维升序排序,然后对第二维归并排序。
设当前合并 \([l,mid],[mid+1,r]\) 区间,此时因为第一维已经排序了,\([l,mid]\) 里面最大 \(a\) 小于 \([mid+1,r]\) 最小 \(a\),现在要计算 \([l,mid]\) 对 \([mid+1,r]\) 区间的贡献,具体做法就是从 \([mid+1,r]\) 中取出一个数时看一眼 \([l,mid]\) 已经被取出了几个数,然后直接加入 \([mid+1,r]\) 中取出的数的答案即可。
三维偏序:三维坐标系 \(n\) 个点,第 \(i\) 个点坐标 \((a_i,b_i,c_i)\),对所有 \(i\) 求多少个 \(j\) 满足 \(a_j<a_i,b_j<b_i,c_j<c_i\)。
还是对第一维排序,之后对第二维归并排序,不同的是现在有第三维的影响,因此考虑用树状数组。
从 \([l,mid]\) 取出时,树状数组上 \(c\) 对应的位置加上 1,从 \([mid+1,r]\) 中取出时,树状数组上对应的区间 \([1,c]\) 中查询和即可,这就是\([l,mid]\) 对这个数的贡献。
四维偏序:四维坐标系 \(n\) 个点,第 \(i\) 个点坐标 \((a_i,b_i,c_i,d_i)\),对所有 \(i\) 求多少 \(j\) 满足 \(a_j<a_i,b_j<b_i,c_j<c_i,d_j<d_i\)。
对第一维排序,然后对第二维和第三维均作归并排序,第四维用树状数组。
此时你有两种做法,第一种是中序遍历版 cdq,第二种是后序遍历版 cdq。
中序遍历版:对于第二维的归并排序 \([l,r]\),此时我们先排 \([l,mid]\),然后开始对第三维做 \([l,r]\) 内的归并排序,仿照上面因为前面区间 \(a,b,c\) 都小于后面区间 \(a,b,c\) 所以第四维可以树状数组,第三维整个做完之后再做第二维上的 \([mid+1,r]\) 的归并排序。
后序遍历版就是将上面第二维 \([l,mid],[mid+1,r]\) 全部排完之后再做第三维 \([l,r]\) 排序。
对于题设而言,使用后序遍历版也可以,但是如果涉及到一些奇怪的 dp,那么就必须使用中序遍历版,因为 dp 过程中我们需要保证前面的所有答案都已经被计算正确,比如说如果四维偏序下我们要做 dp \(f_{i}=\max\{f_j+a_i\}\)(四维偏序下 \(j<i\)),要保证 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 分治:首先对第一维排序,然后中间维通过归并排序消除影响,注意先排 \([l,mid]\) 然后对下一维做 \([l,r]\) 归并排序最后在这一维排 \([mid+1,r]\),倒数第二维正常排序,最后一维树状数组。