CDQ 分治思想
零、引入
CDQ 分治是一种思想而非算法。
- 它可以化动为静。(转为离线问题解决)
- 可以解决偏序等点对计数问题。
- 优化 1D/1D 动态规划的转移。
归并排序 & 逆序对
回顾归并排序求逆序对的过程,也用了 CDQ 分治思想处理 偏序点对计数问题。
把当前区间 \([l, r]\) 分为两个子区间 \([l, mid], [mid + 1, r]\) 先分别求解。
接下来计算左边区间对右边区间的贡献,同时归并两个子区间的元素。
这就是 CDQ 分治最本质的逻辑:
- 递归求解 \([l, mid]\) 区间的答案。
- 计算 \([l, mid]\) 区间对 \([mid + 1, r]\) 区间的影响。
- 计算 \([mid + 1, r]\) 内部的答案。
不难发现这样计算不重不漏。
分治 FFT/NTT 可能是朴素分治,也可能是 CDQ 分治,取决于应用场景:
- 朴素分治套一个华丽的 FFT/NTT:计算一堆多项式的乘积,直接用多项式乘法把 \([l, mid]\) 和 \([mid+1, r]\) 合并。
- CDQ 分治套一个丑陋的 FFT/NTT:本质上是 CDQ 分治优化 DP,只不过再套了一个 FFT 加速转移而已。
一、点对计数
逆序对就是二维偏序问题。
考虑三维偏序怎么做?
例 1:洛谷 P3810 【模板】三维偏序(陌上花开)
首先将同类项(\(a,b,c\) 都相等的元素)合并,并记录它出现的次数 \(cnt\)。
然后要统计三维偏序,这很头痛,所以考虑对每一维把它分别排序,并保证之前维度排序的相对有效性。
由于是统计点对之间的信息,可以联想到 CDQ 分治。
假设已知 \([l, mid]\) 和 \([mid + 1, r]\) 的答案,现在要计算两个区间之间的答案。
考虑在分治开始之前对 \(a\) 排序,这样就只需要统计两维。
此时左右区间的 \(a\) 一定是相对有序的,此时把它们分别对 \(b\) 升序排序,然后用一个类似归并排序的双指针算法,保证 \(b\) 相对有序。
接着按 \(c\) 把元素加入到一个维护值域出现次数的数据结构里,一般使用树状数组。
复杂度 \(O(n \log n)\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, Max, m;
struct node {
int a, b, c;
int cnt, ans;
} p[N], a[N];
bool cmp1(node a, node b) {
if (a.a != b.a) return a.a < b.a;
return (a.b ^ b.b) ? (a.b < b.b) : (a.c < b.c);
}
inline bool cmp2(node a, node b) { return (a.b ^ b.b) ? (a.b < b.b) : (a.c < b.c); }
int ans[N];
int tr[N];
inline void add(int x, int d) { for ( ; x < N; x += x & -x) tr[x] += d; }
inline int query(int x) { int res = 0; for ( ; x ; x -= x & -x) res += tr[x]; return res; }
void cdq(int l, int r) {
if (l == r) return;
int mid = l + r >> 1;
cdq(l, mid), cdq(mid + 1, r);
sort(a + l, a + 1 + mid, cmp2);
sort(a + mid + 1, a + r + 1, cmp2);
int j1 = l, j2 = mid + 1;
while (j2 <= r) {
while (j1 <= mid && a[j1].b <= a[j2].b) add(a[j1].c, a[j1].cnt), j1++;
a[j2].ans += query(a[j2].c);
j2++;
}
for (int i = l; i < j1; i++) add(a[i].c, -a[i].cnt);
}
int main() {
scanf("%d%d", &n, &Max);
for (int i = 1; i <= n; i++) scanf("%d%d%d", &p[i].a, &p[i].b, &p[i].c);
sort(p + 1, p + 1 + n, cmp1);
for (int i = 1; i <= n; i++) {
if (p[i].a == a[m].a && p[i].b == a[m].b && p[i].c == a[m].c) a[m].cnt++;
else a[++m] = p[i], a[m].cnt = 1;
}
cdq(1, m);
for (int i = 1; i <= m; i++) ans[a[i].ans + a[i].cnt - 1] += a[i].cnt;
for (int i = 0; i < n; i++) printf("%d\n", ans[i]);
return 0;
}
例 2:洛谷 P4390 [BalkanOI 2007] Mokia 摩基亚
维护一个平面直角坐标系,支持单点加,求矩阵和。
首先看到矩阵和不难想到:拆成四个二维前缀和。
接下来就是查询 \(x \in [1, x_q], y \in [1, y_q]\) 的所有数之和,这很二维树状数组,但时空有点炸裂。
不妨离线下来考虑。
设 \(t\) 表示该操作进行的时间,则对于一次询问 \((t_q, x_q, y_q)\),能对它产生贡献的点 \((t, x, y)\) 需要满足:
这就是三维偏序板子了。
例 3:洛谷 P3157 [CQOI2011] 动态逆序对
注意到已经有两维偏序 \(i \lt j\) 和 \(a_i \gt a_j\)。
由于只删除 \(m\) 个数,可以先分类处理贡献(设 \(A\) 集合是没有被删除的数集,\(B\) 是删除的数集):
- \(A-A\)。
- \(A-B\)。
- \(B-B\)。
前两者是好处理的,关键在于 \(B-B\) 有三维偏序。
设 \(T_i\) 表示 \(i\) 被删除的时间。
数对 \((i,j)\) 有贡献仅当满足以下两组条件之一:
- \(i \gt j, T_i \lt T_j, a_i \lt a_j\)
- \(i \lt j, T_i \lt T_j, a_i \gt a_j\)
发现两组条件都有 \(T_i \lt T_j\),所以考虑最外层对 \(T\) 升序排序。
至于另外两维,就是三维偏序了,分别做一遍 CDQ 即可。
例 4:洛谷 P8575 「DTOI-2」星之河
很魔怔的树上二维偏序。
如果直接对树 dfs 同时维护信息显然是没有什么道理的,因为 CDQ 擅长解决的是静态问题,在树上 dfs 还得动态插入删除。
但已经很明显有二维偏序了,不妨找找第三维?
利用子树 dfn 连续的性质,\(u\) 子树的 dfn 区间为 \([dfn_u, dfn_u + sz_u - 1]\)。
这其实也是三维偏序,只不过第三维 dfn 加了一个区间限制而已。
还是一样把树拍扁到序列上,做三维偏序,令 dfn 为第三维,树状数组查询区间和。
例 5:k 维偏序
好恶心的东西。
例 1 中三维偏序的最内层套了一个树状数组,但事实上这题也可以 CDQ 套 CDQ。
具体地,最外层还是按第一维排序。
外层 CDQ1 处理第二维,先将 \([l, mid]\) 和 \([mid+1, r]\) 执行 CDQ1 按第二维排序,再归并两个区间。
然后给左区间所有数打 \(0\) 标记,右区间所有数打上 \(1\) 标记。
接着执行 CDQ2 处理第三维,还是先递归 CDQ2 分别将 \([l, mid]\) 和 \([mid + 1, r]\) 按第三维排序,再归并。
注意归并的时候,只有右区间且有 \(1\) 标记的才能统计答案。
复杂度为 \(O(n \log^2 n)\),劣于树状数组直接维护的 \(O(n \log n)\)。
如何处理 \(k\) 维偏序?
依然考虑 CDQ 套 CDQ。
- 外层的 CDQ 负责打标记、归并。
- 最内层的 CDQ 负责统计答案。
- 当然,最内层也可以用数据结构(如树状数组)优化,常数会小很多。
暴力套 \(k-1\) 次的 \(k\) 维偏序是 \(O(n \log^{k-1} n)\) 的。
当 \(k\) 较大的时候复杂度会很劣,甚至不如分块。
二、优化 1D/1D 动态规划转移
1D/1D 动态规划指的是一类特定的 DP 问题,该类题目的特征是 DP 数组是一维的,转移是 \(O(n)\) 的。如果条件良好的话,有时可以通过 CDQ 分治来把它们的时间复杂度由 \(O(n^2)\) 降至 \(O(n \log^2 n)\)。——OI-Wiki
由于是动态规划,需要保证状态转移的有序性,所以算法流程大致如下:
- 递归处理 \([l, mid]\) 的 DP 值。
- 计算 \([l, mid]\) 对 \([mid + 1, r]\) 的影响(此时保证了 \([l, mid]\) 已经被计算完)。
- 递归处理 \([mid + 1, r]\) 的 DP 值。
不能先分治两边再计算左对右贡献。
例 6:简单应用
该题来自 OI-Wiki。
例如,给定一个序列,每个元素有两个属性 \(a, b\)。我们希望计算一个 DP 式子的值,它的转移方程如下:
\[dp_{i}=1+ \max_{j=1}^{i-1} dp_{j} [a_j \lt a_i] [b_j \lt b_i] \]
朴素转移 \(O(n^2)\),发现式子其实又可以写成这样:
满足三维偏序结构,且为 1D/1D 动态规划,可以用 CDQ 分治优化 DP 转移。
具体地,当分治区间 \(l = r\) 时到达边界,直接 \(dp_l \leftarrow dp_l + 1\) 即可。
否则和模板一样用树状数组维护 \([l, mid]\) 对 \([mid+1, r]\) 的贡献,具体地,用前缀 \(\max\) 树状数组。
例 7:洛谷 P3364 Cool loves touli
题目表述有点绕,大致意思是每个点有 \(l, a, b, c\) 四个属性,需要按 \(l\) 从小到大挑出一些点,使得选出的点 \(j, i\) 在选出序列相邻,且满足 \(c_j \leq a_i, c_i \geq b_j\),需要最大化选择数量。
直接按照题意做 DP,设 \(dp_i\) 表示前 \(i\) 个强制选 \(i\) 能选出最多的点数。
dp[1] = 1;
for (int i = 2; i <= n; i++)
for (int j = 1; j < i; j++)
if (a[j].c <= a[i].a && a[i].c >= a[j].b) dp[i] = max(dp[i], dp[j] + 1);
for (int i = 1; i <= n; i++) ans = max(ans, dp[i]);
乍看之下是一个四维偏序,但是再仔细一看,它其实是一个不同维度之间的三维偏序,它只有三个限制条件。
而且它是 1D/1D 动态规划,可以 CDQ 分治优化转移。
总体对 \(l\) 排序,然后分成左右两个区间,左边对 \(c\) 升序,右边对 \(a\) 升序排序。
至此,已经完成了 \(l_j \lt l_i\) 和 \(c_j \leq a_i\) 两维,前者是由于先前已经对 \(l\) 升序排序;后者类似归并排序双指针。
还差一个 \(b_j \leq c_i\)?这也是简单的,和模板一样直接树状数组即可。
当 \(l=r\) 是递归边界,记得是和 \(1\) 取 \(\max\) 而不是赋值。
例 8:洛谷 P2487 [SDOI2011] 拦截导弹
先考虑第一问,若 \(i \lt j, a_i \leq a_j, b_i \leq b_j\) 则建边 \(i \to j\),求最长路径。
这是好做的,显然三维偏序,DP 转移,和之前的问题一样。
然后第二问:对于每一个节点 \(i\),求 \(i\) 在路径上的概率。
保证总方案数不超过 C++ 中 double 类型的存储范围。
所以直接用 double 存总方案数即可,再设 \(f_i, g_i\) 表示 \(1 \to i\) 和 \(i \to \text{end}\) 的方案数,显然这两段路径都要长度极大,才能满足总长度极大。
然后一样地三维偏序过程中记录方案数之和即可。
具体实现:分别正反做 cdq 分治,按照正常三维偏序的板子套计数即可。
一开始方案数忘记加上第三维的限制了(只拿了一个桶记)所以要把它和最大值一起扔一个 \(\text{pair}\) 维护。
例 9:洛谷 P3769 [CH弱省胡策R2] TATT
求四维最长不下降子序列长度。
一个很直接的做法是:一眼看出是一个 1D/1D 动态规划,并且四维偏序直接 CDQ 套 CDQ 就行了。
然后翻了翻题解区,发现还有其他做法:CDQ 分治干掉两维,还剩两维动态开点线段树套树状数组。
还有 K-D Tree 做法,暂时不会。
先按 \(a\) 为第一关键字排序,不难想到 DP。
转移是四维偏序,不难想到 CDQ 套 CDQ。
注意去重,变成每个点有 \(\text{cnt}\) 这个权值。
具体地,最外层按 \(a\) 排,第一层 CDQ 按 \(b\) 排,第二层 CDQ 按 \(c\) 排,同时树状数组维护第四维 \(d\)。
但是排到第二维时第一维乱序了。
不妨给每个点打上一个标记 \(\text{opt}\),在第一层 CDQ 第一维有序的时候,分成左右两部分,左边 \(\text{opt}=0\),右边为 \(1\)。
第二层钦定只有 \(0\) 能向 \(1\) 转移贡献即可。
简单地说,就是把 \(\{ a, b, c, d \}\) 的偏序问题转化为 \(\{ 0/1, b, c, d \}\) 的偏序问题。
注意使用 stable_sort
。
三倍经验: 洛谷 P4849 寻找宝藏 、 洛谷 P5621 [DBOI2019] 德丽莎世界第一可爱 。
例 10:洛谷 P4093 [HEOI2016/TJOI2016] 序列
很简单的 CDQ 分治优化 1D/1D 动规转移。
每次只能操作一个,不妨先求出每个位置能操作到的最大值和最小值(\(\text{mx, mn}\))分别是多少。
设 \(dp_i\) 表示以 \(i\) 结尾的最长子序列长度,则:
后面的条件很明显是类似于三维偏序的东西,直接做就行了。
具体地,和 洛谷 P3364 的思路一样,四维但只有三个偏序关系,中途 DP 转移一下即可。
三、化动为静
把动态的问题离线下来,利用 CDQ 分治解决前面操作对后面查询的影响。
本质上也是点对问题!把操作看作 \(1\) 点,询问看作 \(2\) 点,计算所有 \((1, 2)\) 点对的贡献,存在 \(2\) 上。
例 11:洛谷 P4169 [Violet] 天使玩偶/SJY摆棋子
动态加点,查询距离某个点曼哈顿距离最近的点距,非常没道理。
但是可以离线下来,对时间轴分治,计算先加入的点对后询问的点的贡献。
但是这样只有一维时间轴偏序关系,很难在 CDQ 内部直接维护最近的点。
不过 \(x, y\) 维可以“构造”出一个偏序关系,即我们只算以查询点为原点时第三象限的贡献,把坐标轴旋转 \(4\) 次,每次 \(90\) 度即可。
当然如果你要写 4 个 CDQ 我也不拦着你。