CDQ 分治 & K-D Tree 学习笔记
这两算法都是在将近一年前就学过了,后面忘了,最近才重新学会来。鉴于这两个算法是我同时期学的,那就把它们两个放一起吧。
1. CDQ 分治
离线算法,用于处理多维偏序的。嗯对。
1.1 二维偏序
统计 \(i,j\in P\) 且 \(a_i<a_j\),\(b_i<b_j\) 的 \(i,j\) 有多少对。
首先按 \(a\) 排序,然后对 \([1,n]\) 分治。
分治内容:
假如当前区间为 \(l,r\),那么它处理的就是 \([l,mid]\) 部分对于 \([mid+1,r]\) 部分。所以需要递归分治 \([l,mid]\) 和 \([mid+1,r]\)。
首先把 \([l,mid]\) 和 \([mid+1,r]\) 按 \(b\) 排序。
此时,\([l,mid]\) 中 \(a\) 的大小一定小于 \([mid+1,r]\) 中 \(a\) 的大小。
然后开始按 \(b\) 归并。
每次轮到 \([mid,r]\) 中入桶的时候,统计 \([l,mid]\) 已经有多少个在桶里了,这个就是 \([l,mid]\) 对 \([mid+1,r]\) 的贡献。
时间复杂度 \(O(n \log^2 n)\)。
考虑优化一下。
不难发现,\([l,mid]\) 和 \([mid+1,r]\) 归并排好的数组正好可以为 \([l,r]\) 归并时所用。
好吧这就是归并排序的板子。
所以复杂度可以做到 \(O(n \log n)\)。
几乎没有应用范围,所以不给代码了。
1.2 三维偏序
统计 \(i,j\in P\) 且 \(a_i<a_j\),\(b_i<b_j\),\(c_i<c_j\) 的 \(i,j\) 有多少对。
其实做法差不多。
同样先按 \(a\) 排序,然后开始分治。
假如当前区间为 \(l,r\),那么它处理的就是 \([l,mid]\) 部分对于 \([mid+1,r]\) 部分,需要递归分治 \([l,mid]\) 和 \([mid+1,r]\)。
依旧先把 \([l,mid]\) 和 \([mid+1,r]\) 按 \(b\) 排序。
然后开始按 \(b\) 归并。
每次轮到 \([mid,r]\) 中 \(j\) 入桶的时候,统计 \([l,mid]\) 已经有在桶里的有多少个 \(i\) 满足 \(c_i<c_j\),这个就是 \([l,mid]\) 对 \([mid+1,r]\) 的贡献。
\(c_i\) 的维护可以用树状数组和离散化,每当 \([l,mid]\) 中 \(i\) 加入桶中时,树状数组加入 \(c_i\),\([mid,r]\) 中的 \(j\) 加入桶中时查询时查询小于 \(c_j\) 的 \(i\) 的个数。
时间复杂度 \(O(n \log^2 n)\)。
由于瓶颈在树状数组而不是排序,所以这个复杂度只能做到 \(\log^2\) 的了。
1.3 四维偏序
统计 \(i,j\in P\) 且 \(a_i<a_j\),\(b_i<b_j\),\(c_i<c_j\),\(d_i<d_j\) 的 \(i,j\) 有多少对。
此时需要 CDQ 套 CDQ。
依旧按 \(a\) 排序,对 \([1,n]\) 进行分治。
第一层分治 \([l,r]\) 时,先将 \([l,mid]\) 打上修改标记,\([mid+1,r]\) 打上查询标记,然后再把 \([l,r]\) 分治一遍。
第二层分治 \([l,r]\) 时,将 \([l,r]\) 按 \(b\) 排序,此时 \([l,mid]\) 内的 \(b\) 一定小于 \([mid+1,r]\) 中的 \(b\)。
然后按 \(c\) 归并排序:
每次轮到 \([mid,r]\) 中带有查询标记的 \(j\) 入桶的时候,统计 \([l,mid]\) 已经有在桶里的有多少个带有修改标记的 \(i\) 满足 \(d_i<d_j\),这个就是 \([l,mid]\) 对 \([mid+1,r]\) 的贡献。
这个同样可以用树状数组和离散化做,每当 \([l,mid]\) 中带有修改标记的 \(i\) 加入桶中时,树状数组加入 \(d_i\),\([mid,r]\) 中带有查询标记的 \(j\) 加入桶中时查询时查询小于 \(d_j\) 的 \(i\) 的个数。
时间复杂度 \(O(n \log^3 n)\)。
1.4 注意事项
- 分治时需要合并重复点,或者使用
stable_sort。 - 树状数组撤销时只能在 \([l,r]\) 中一个一个撤销,不然复杂度会炸。
1.5 应用
某些时候,下标,修改操作的时间,都可以作为偏序的对象。
1.5.1 二维偏序
非常典型的应用就是逆序对,但是直接用树状数组的写法比 CDQ 好多了。
其余的时候 K-D Tree 比这玩意好用多了,CDQ 能做的 K-D Tree 能做,CDQ 不能做的 K-D Tree 也能做。
1.5.2 三维偏序
P4390 [BalkanOI 2007] Mokia 摩基亚。
这题没拿 CDQ 写,其实可以将操作的时间也看作一维,这样就能进行愉快的分治了。
与上一题同理。
1.5.3 四维偏序
用的也不多。
模板题:P14957 【模板】离线静态四维数点、P5621 [DBOI2019] 德丽莎世界第一可爱。
1.6 CDQ 优化 DP
不太会。
反正就是归并的时候转移一下状态,然后注意归并顺序是 \([l,mid]\)、\([l,r]\)、\([mid+1,r]\) 就行了。
2. K-D Tree
用于处理一些高维问题的数据结构,在信息学中,维度通常为 \(2\) 或 \(3\)。
K-D Tree 是一颗二叉树,上的每个点都代表了一个 \(k\) 维空间的一个点和一个包含这个点超长方体。
2.1 建树
摘自 oi-wiki:
假设我们已经知道了 \(k\) 维空间内的 \(n\) 个不同的点的坐标,要将其构建成一棵 K-D Tree,步骤如下:
- 若当前超长方体中只有一个点,返回这个点.
- 选择一个维度,将当前超长方体按照这个维度分成两个超长方体。
- 选择切割点:在选择的维度上选择一个点,这一维度上的值小于这个点的归入一个超长方体(左子树),其余的归入另一个超长方体(右子树)。
- 将选择的点作为这棵子树的根节点,递归对分出的两个超长方体构建左右子树,维护子树的信息。
建树复杂度 \(O(n\log n)\)。
2.2 插入
如果直接暴力插入节点,可能会造成子树大小不平衡的问题,从而导致复杂度错误。
有三种解决方法:
- 替罪羊树式处理,当大小不平衡到一定阈值时,直接将整棵树拍扁重构。感觉不算很好写,常数也很大。
- 根号重构法:插入的时候,先存下来要插入的点,每根号次插入进行一次重构,复杂度我不会证,可以去 oi-wiki 上看。
- 二进制分组法:维护若干个 \(2^x\) 大小的 K-D Tree,插入时直接建一颗大小为 \(1\) 的新树,如果有两颗大小相同的树就直接把它们合并成一个大的树。复杂度是 \(O(n^{1-\frac{1}{k}})\),我同样不会证,可以去 oi-wiki 上看。
2.3 查询
同样摘自 oi-wiki:
在查询高维矩形区域内的所有点的一些信息时,需要记录每个结点子树内每一维度上的坐标的最大值和最小值。
- 如果当前子树对应的矩形与所求矩形没有交点,就直接跳过。
- 如果当前子树对应的矩形完全包含在所求矩形内,返回当前子树所存的答案。
- 否则,判断当前点是否在所求矩形内,更新答案并递归在左右子树中查找答案。
复杂度 \(O(n^{1-\frac{1}{k}})\),我同样不会证,可以去 oi-wiki 上看。
总结:我是 fvv。
2.4 例题
寻找平面内离某点最近 / 最远的点:只须查询时,看当前点所在的矩形内是否可能存在小于 / 大于当前答案的点,如果没有,直接跳过,否则就两个子树都搜一遍。
模板题:P1429 平面最近点对(加强版)、P2479 [SDOI2010] 捉迷藏。
直接使用优先队列暴力维护前 \(k\) 个距离最远的点即可。
考虑使用权值线段树套 K-D Tree。
先对每个权值进行离散化,然后对于每一个线段树上的节点 \([l,r]\) 维护权值在 \([l,r]\) 内的点,查询时使用线段树上二分,找到点 \(t\) 使得 \([t,n]\) 中在矩形 \(x,y,x2,y2\) 的点的个数恰好 \(\ge k\),插入就对线段树上每个节点的 K-D Tree 都插入一遍。
时间复杂度 \(O(n\sqrt n\log n)\),需要卡常。
3. 总结
我也不知道要总结什么。

浙公网安备 33010602011771号