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 注意事项

  1. 分治时需要合并重复点,或者使用 stable_sort。
  2. 树状数组撤销时只能在 \([l,r]\) 中一个一个撤销,不然复杂度会炸。

1.5 应用

某些时候,下标,修改操作的时间,都可以作为偏序的对象。

1.5.1 二维偏序

非常典型的应用就是逆序对,但是直接用树状数组的写法比 CDQ 好多了。

其余的时候 K-D Tree 比这玩意好用多了,CDQ 能做的 K-D Tree 能做,CDQ 不能做的 K-D Tree 也能做。

1.5.2 三维偏序

模板题:P3810 【模板】三维偏序 / 陌上花开。

P4390 [BalkanOI 2007] Mokia 摩基亚。

这题没拿 CDQ 写,其实可以将操作的时间也看作一维,这样就能进行愉快的分治了。

P3157 [CQOI2011] 动态逆序对。

与上一题同理。

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 插入

如果直接暴力插入节点,可能会造成子树大小不平衡的问题,从而导致复杂度错误。

有三种解决方法:

  1. 替罪羊树式处理,当大小不平衡到一定阈值时,直接将整棵树拍扁重构。感觉不算很好写,常数也很大。
  2. 根号重构法:插入的时候,先存下来要插入的点,每根号次插入进行一次重构,复杂度我不会证,可以去 oi-wiki 上看。
  3. 二进制分组法:维护若干个 \(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] 捉迷藏。

P2093 [国家集训队] JZPFAR。

直接使用优先队列暴力维护前 \(k\) 个距离最远的点即可。

P4848 崂山白花蛇草水。

考虑使用权值线段树套 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. 总结

我也不知道要总结什么。

posted @ 2026-09-30 17:00  doooge  阅读(7)  评论(0)    收藏  举报