Lovi.space

算法

CDQ 分治:在高维偏序中制造可统计的秩序

三维偏序计数为什么能被拆成分治、归并与树状数组三种熟悉工具的协作。

·1 min read
#C++14#算法#CDQ 分治#树状数组

核心洞察:CDQ 分治的力量不在“多一种模板”,而在于它主动创造了一个方向已确定、另一个方向可查询的局部秩序。 设有许多点,每个点带三维坐标 (a,b,c)`(a, b, c)`,需要统计满足 ajai,bjbi,cjci`a_j \le a_i, b_j \le b_i, c_j \le c_i` 的点对。暴力枚举需要三重比较;仅按一个维度排序,又无法同时保证另外两个维度的贡献关系。

先把一个维度交给递归

将点按 a 排序后,CDQ 在下标区间上分治。处理左半边与右半边的交叉贡献时,左边的点天然拥有不大于右边的 a;第一个维度已经被递归边界固定。 接下来按 b 做归并。扫描过程中,左侧 b 不大于当前右侧点的元素被加入 Fenwick Tree,树状数组维护它们在 c 维上的数量。于是查询前缀和即可得到第三维也满足条件的贡献。

while (i <= mid && j <= right) {
if (points[i].b <= points[j].b) {
bit.add(points[i].c, points[i].count);
++i;
} else {
points[j].answer += bit.sum(points[j].c);
++j;
}
}

这里的关键不只是代码顺序,而是语义:左边是已经发生、可以贡献的点;右边是正在提问、等待累计答案的点。

为什么要合并重复点

完全相同的三维坐标互相满足偏序。如果逐个处理,不仅会重复做工作,还容易把同坐标的先后顺序错误地计数。应先排序并合并相同点,用 count 表示出现次数,最后再把总答案按次数还原。

三种工具各做一件事

工具 固化的关系
CDQ 分治 左侧 `a` 不大于右侧 `a`
归并过程 加入时保证 `b` 有序
Fenwick Tree 查询 `c` 的前缀贡献
这种分工让原本耦合的三维条件逐层解除。最终复杂度为 `O(n log^2 n)`:递归层数一个 `log n`,每层树状数组操作再乘一个 `log n`。 ## 常见失败原因 - `c` 未离散化,树状数组下标过大或为负。 - 合并后没有清理 Fenwick Tree,贡献泄漏到下一轮。 - `a` 相等时分治边界处理不当,导致同层点对重复或遗漏。 ## 结语 CDQ 分治的美感来自秩序的制造。它没有直接解决三维问题,而是让每一层只处理一个已经被约束过的更小问题。这种思想同样适用于扫描线、离线查询与许多“看起来无法同时排序”的任务。

读到这里,说明你也在认真对待这个问题。

讨论与反应

⌘ K