CDQ分治
CDQ 分治
简介
CDQ 分治是一种思想而不是具体的算法,与动态规划类似。目前这个思想的拓展十分广泛,依原理与写法的不同,大致分为三类:
- 解决和点对有关的问题。
- 1D 动态规划的优化与转移。
- 通过 CDQ 分治,将一些动态问题转化为静态问题。
CDQ 分治的思想最早由 IOI2008 金牌得主陈丹琦在高中时整理并总结,它也因此得名。
解决和点对有关的问题
这类问题多数类似于「给定一个长度为 \(n\) 的序列,统计有一些特性的点对 \(\left(i,j\right)\) 的数量/找到一对点 \(\left(i,j\right)\) 使得一些函数的值最大」。
CDQ 分治解决这类问题的算法流程如下:
- 找到这个序列的中点 \(mid\);
- 将所有点对 \(\left(i,j\right)\) 划分为 3 类:\(1\le i \le mid,1\le j\le mid\) 的点对;\(1\le i\le mid,mid+1\le j\le n\) 的点对;\(mid+1\le i\le n,mid+1\le j\le n\) 的点对。
- 将 \(\left(1,n\right)\) 这个序列拆成两个序列 \(\left(1,mid\right)\) 和 \(\left(mid+1,n\right)\)。此时第一类点对和第三类点对都在这两个序列之中;
- 递归地处理这两类点对;
- 设法处理第二类点对。
可以看到 CDQ 分治的思想就是不断地把点对通过递归的方式分给左右两个区间。在实际应用时,我们通常使用一个函数 \(solve\left(l,r\right)\) 处理 \(l\le i\le r,l\le j\le r\) 的点对。上述算法流程中的递归部分便是通过 \(solve\left(l,mid\right)\) 与 \(solve\left(mid,r\right)\) 来实现的。剩下的第二类点对则需要额外设计算法解决。
例题
三位偏序
解题思路
三维偏序是 CDQ 分治的经典问题。
题目要求统计序列里点对的个数,那试一下用 CDQ 分治。
首先将序列按 \(a\) 排序。
假设我们现在写好了 \(solve\left(l,r\right)\),并且通过递归搞定了 \(solve\left(l,mid\right)\) 和 \(solve\left(mid+1,r\right)\)。现在我们要做的,就是统计满足 \(l\le i\le mid,mid+1\le j\le r\) 的点对 \(\left(i,j\right)\) 中,有多个点对还满足 \(a_i\le a_j,b_i\le b_j,c_i\le c_j\) 的限制条件。
稍微思考一下就会发现,那个 $a_i $ 的限制条件没啥用了:已经将序列按 \(a\) 排序,则 \(a_i\le a_j\) 可转化为 \(i\le j\)。\(i\) 比 \(mid\) 小,\(j\) 比 \(mid\) 大,那 \(i\) 肯定比 \(j\) 要小。现在还剩下两个限制条件:\(b_i\le b_j\) 与 \(c_i\le c_j\) , 根据这个限制条件我们就可以枚举 \(j\), 求出有多少个满足条件的 \(i\)。
为了方便枚举,我们把 \(\left(l,mid\right)\) 和 \(\left(mid+1,r\right)\) 中的点全部按照 \(b\) 的值从小到大排个序。之后我们依次枚举每一个 \(j\) , 把所有 \(b_i\le b_j\) 的点 \(i\) 全部插入到某种数据结构里(这里我们选择树状数组)。此时只要查询树状数组里有多少个点的 \(c\) 值是小于 \(c_j\) 的,我们就求出了对于这个点 \(j\),有多少个 \(i\) 可以合法匹配它了。
当我们插入一个 \(c\) 值等于 \(x\) 的点时,我们就令树状数组的 \(x\) 这个位置单点 \(+ 1\),而查询树状数组里有多少个点小于 \(x\) 的操作实际上就是在求前缀和,只要我们事先对于所有的 \(c\) 值做了离散化,我们的复杂度就是对的。
对于每一个 \(j\),我们都需要将所有 \(b_i\le b_j\) 的点 \(i\) 插入树状数组中。由于所有的 \(i\) 和 \(j\) 都已事先按照 \(b\) 值排好序,这样的话只要以双指针的方式在树状数组里插入点,则对树状数组的插入操作就能从 \(O\left(n^2\right)\) 次降到 \(O\left(n\right)\) 次。
通过这样一个算法流程,我们就用 \(O\left(n\log n\right)\) 的时间处理完了关于第二类点对的信息了。
示例代码
#include <algorithm>
#include <cstdio>
const int maxN = 1e5 + 10;
const int maxK = 2e5 + 10;
int n, k;
struct Element {
int a, b, c;
int cnt;
int res;
bool operator!=(Element other) {
if (a != other.a) return true;
if (b != other.b) return true;
if (c != other.c) return true;
return false;
}
};
Element e[maxN];
Element ue[maxN];
int m, t;
int res[maxN];
struct BinaryIndexedTree {
int node[maxK];
int lowbit(int x) { return x & -x; }
void Add(int pos, int val) {
while (pos <= k) {
node[pos] += val;
pos += lowbit(pos);
}
return;
}
int Ask(int pos) {
int res = 0;
while (pos) {
res += node[pos];
pos -= lowbit(pos);
}
return res;
}
} BIT;
bool cmpA(Element x, Element y) {
if (x.a != y.a) return x.a < y.a;
if (x.b != y.b) return x.b < y.b;
return x.c < y.c;
}
bool cmpB(Element x, Element y) {
if (x.b != y.b) return x.b < y.b;
return x.c < y.c;
}
void CDQ(int l, int r) {
if (l == r) return;
int mid = (l + r) / 2;
CDQ(l, mid);
CDQ(mid + 1, r);
std::sort(ue + l, ue + mid + 1, cmpB);
std::sort(ue + mid + 1, ue + r + 1, cmpB);
int i = l;
int j = mid + 1;
while (j <= r) {
while (i <= mid && ue[i].b <= ue[j].b) {
BIT.Add(ue[i].c, ue[i].cnt);
i++;
}
ue[j].res += BIT.Ask(ue[j].c);
j++;
}
for (int k = l; k < i; k++) BIT.Add(ue[k].c, -ue[k].cnt);
return;
}
int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++) scanf("%d%d%d", &e[i].a, &e[i].b, &e[i].c);
std::sort(e + 1, e + n + 1, cmpA);
for (int i = 1; i <= n; i++) {
t++;
if (e[i] != e[i + 1]) {
m++;
ue[m].a = e[i].a;
ue[m].b = e[i].b;
ue[m].c = e[i].c;
ue[m].cnt = t;
t = 0;
}
}
CDQ(1, m);
for (int i = 1; i <= m; i++) res[ue[i].res + ue[i].cnt - 1] += ue[i].cnt;
for (int i = 0; i < n; i++) printf("%d\n", res[i]);
return 0;
}