CDQ 分治思想

零、引入

CDQ 分治是一种思想而非算法。

  1. 它可以化动为静。(转为离线问题解决)
  2. 可以解决偏序等点对计数问题
  3. 优化 1D/1D 动态规划的转移。

归并排序 & 逆序对

回顾归并排序求逆序对的过程,也用了 CDQ 分治思想处理 偏序点对计数问题。
把当前区间 \([l, r]\) 分为两个子区间 \([l, mid], [mid + 1, r]\) 先分别求解。
接下来计算左边区间对右边区间的贡献,同时归并两个子区间的元素。

这就是 CDQ 分治最本质的逻辑:

  1. 递归求解 \([l, mid]\) 区间的答案。
  2. 计算 \([l, mid]\) 区间对 \([mid + 1, r]\) 区间的影响。
  3. 计算 \([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)\) 需要满足:

\[t \leq t_q, x \leq x_q, y \leq y_q \]

这就是三维偏序板子了。

例 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)\),发现式子其实又可以写成这样:

\[dp_{i}=1+ \max_{j=1}^{n} dp_{j} [j \lt i] [a_j \lt a_i] [b_j \lt b_i] \]

满足三维偏序结构,且为 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\) 结尾的最长子序列长度,则:

\[dp_i = \max \{ dp_j \} \quad (j \lt i, \text{mx}_j \leq a_i, a_j \leq \text{mn}_i) \]

后面的条件很明显是类似于三维偏序的东西,直接做就行了。
具体地,和 洛谷 P3364 的思路一样,四维但只有三个偏序关系,中途 DP 转移一下即可。


三、化动为静

把动态的问题离线下来,利用 CDQ 分治解决前面操作对后面查询的影响。
本质上也是点对问题!把操作看作 \(1\) 点,询问看作 \(2\) 点,计算所有 \((1, 2)\) 点对的贡献,存在 \(2\) 上。

例 11:洛谷 P4169 [Violet] 天使玩偶/SJY摆棋子

动态加点,查询距离某个点曼哈顿距离最近的点距,非常没道理。
但是可以离线下来,对时间轴分治,计算先加入的点后询问的点的贡献。
但是这样只有一维时间轴偏序关系,很难在 CDQ 内部直接维护最近的点。
不过 \(x, y\) 维可以“构造”出一个偏序关系,即我们只算以查询点为原点时第三象限的贡献,把坐标轴旋转 \(4\) 次,每次 \(90\) 度即可。
当然如果你要写 4 个 CDQ 我也不拦着你。

例:*洛谷 P3206 [HNOI2010] 城市建设

posted @ 2025-06-28 20:59  Conan15  阅读(69)  评论(0)    收藏  举报