多维数点技术整理

多维数点技术整理

什么是多维数点

我们暂时定义 \(d\) 维数点是这样一个问题,你需要维护一个点集 \(S\subseteq \mathbb R^d\),操作如下:

  • 插入一个 \(\mathbb R^d\) 中的向量 \(\boldsymbol x\)

  • 给定一个 \(\mathbb R^d\) 中的向量 \(\boldsymbol y\),询问有多少个 \(S\) 中的向量 \(\boldsymbol x\) 使得对于每一维 \(i\) 都有 \(\boldsymbol y[i]\geq \boldsymbol x[i]\)(全维度偏序)。

  • 静态和动态的区别:插入全部在询问之前是静态;插入与询问可以交错进行是动态。

  • 在线和离线的区别:给出询问后要立即给出回答的是在线;给出询问后不必立即回答,可以读入所有询问后再回答的是离线。

一般认为动态的问题比静态的问题难,在线的问题比离线的问题难。

  • 带权的问题版本:插入 \(S\) 中的向量有一个权值,询问时需要回答满足条件的向量的权值之和。
  • 区间的问题版本:询问变成给定两个向量 \(\boldsymbol l, \boldsymbol r\),要求变成 \(\boldsymbol l[i]\leq \boldsymbol x[i]\leq \boldsymbol r[i]\)
    • 如果信息是可减的,那区间只是把一个新的询问拆成 \(2^d\) 个旧的询问而已。
    • 如果信息是不可减的,就比较麻烦,涉及到另外的“自由度”或者“side”的问题,暂时没有研究,请阅读李欣隆课件(也暂时找不到)。

之所以我写的是“多维数点”而不是“高维数点”,是因为我觉得维数如果太高了的话做法就比较唯一(bitset),我们还是应该聚焦低维的问题。

转化问题的算法

记号:问题规模的描述

记号:\(T(m)\) 表示一个大小为 \(m\) 的问题。\(kT(m)\) 就是 \(k\) 个大小为 \(m\) 的问题,\(T(m_1)+T(m_2)\) 就是一个 \(m_1\) 问题和一个 \(m_2\) 问题。

记号:\(n, q, m\) 分别是修改次数、询问次数,以及总次数 \(m=n+q\)。还有一个值域 \(V\) 的参数。

手动加强:离线 -> 在线、静态 -> 动态

通过手动声明“该问题为在线问题”以将一个离线问题转为在线问题;通过手动声明“该问题为动态问题”以将一个静态问题转为动态问题。此过程是自然过程,不改变问题的大小。(这段话写出来之后你自己不会笑吗?)

判定问题:无权、判定 -> 降维、带权、查最值

如果询问只关心答案是否为 \(0\)(这时问题不能带权),那么这就是一个判定性问题。可以选择一个维度,将它声明为权值,然后查询权值的最值,这样就降低了一维。

时间维(扫描线):离线动态 d 维 = 离线静态 d+1 维

通过将动态问题的动态属性声明为时间维,从而以升高一维的代价将问题由动转静;或者将问题的其中一维声明为时间维,从而以将问题由静转动的代价将问题降低一维。可以发现后一种情况中,被声明为时间维的维度得是偏序,就像 \(\boldsymbol y[i]\geq \boldsymbol x[i]\),而不是区间;当然,信息有可减性时,区间可以拆成偏序。此过程是自然过程,不改变问题的大小。

离散化: 离线静态、离线动态、在线静态 -> 降低值域

问题不是在线且动态的时候,可以将所有数字拿出来,统一进行排序+二分(也就是离散化)将值域 \(V\) 缩小到 \(O(n)\),复杂度是 \(O(m\log n)\)

cdq 分治:离线动态 -> 离线静态

如果修改对询问的贡献是独立的(例如多维数点),可以使用 cdq 分治将 \(m\) 个修改和询问的离线动态问题转化为 \(\sum_{i\geq 0}2^iT(m/2^i)\) 的离线静态问题。

这里我们之所以要把具体拆分的问题数量写出来,是因为那并不总是 \(=T(m)\log m\)。例如 \(T(m)=m\sqrt m\) 的话,和式就等于 \(O(m\sqrt m)\) 而不是 \(O(m^{3/2}\log m)\) 或者其它别的东西。

过程:对于 \(m\) 个操作,先将前 \(m/2\) 个操作里的修改操作,和后 \(m/2\) 个操作里的询问操作,分别提取出来,形成新的离线静态问题 \(T(m)\)。然后将前 \(m/2\) 个操作和后 \(m/2\) 个操作分别递归下去。\(m\leq 1\) 为递归的边界。

二进制分组:动态 -> 静态

同样,如果修改对询问的贡献是独立的,可以使用二进制分组将 \(m\) 个修改和询问的动态问题转化为若干个静态问题,这里问题的具体数量有点难以衡量。假如有 \(n\) 个插入操作,则我们在全过程中会产生 \(\sum_{i\geq 0}n/2^i\cdot T(2^i)\) 这么多个静态问题,但是同一时刻问题的大小的总和不超过 \(n\),然后我们每次询问都要在 \(\sum_{2^i\leq n}T(2^i)\) 这些问题中查询(最坏情况)。

过程:维护若干个可以在线查询的静态问题,大小都是 \(2\) 的次幂。每次插入操作,就插入一个 \(T(1)\) 的问题,并维护:如果同时存在两个 \(T(2^i)\) 问题,则舍弃它们并重建一个 \(T(2^{i+1})\) 问题。每次查询就在所有的问题中分别查询。

扩展:如果题目操作除了插入还有删除怎么办?插入 \(2^k−1\) 个点后,重复插入删除,每次操作都要重构 \(2^k−1\) 个点,那复杂度就假了。

对于数点这种有可减性的信息,我们可以对删除操作另维护一组负贡献的二进制分组,查询时两组做差。

若信息不具有可减性,但是删除操作是撤销上一次插入,那么可以考虑懒惰合并,即当有 \(3\) 个 \(k−1\) 级组时合并其中 \(2\) 组至 \(k\) 级,这样每 \(O(2^k)\) 次操作才会重构一次 \(k\) 级组,均摊复杂度还是 \(O(\log n)\) 的。——题解:P3810 【模板】三维偏序(陌上花开) - 洛谷专栏

可以发现:二进制分组其实和 cdq 分治是同样的东西。

定期重构(操作序列分块):动态 -> 静态 + 较小的动态

同样,如果修改对询问的贡献是独立的,可以使用定期重构(需要取一个参数 \(B\))将 \(m\) 个修改和询问的动态问题转化为 \(m/B\cdot T(m)\) 的静态问题,加上 \(m/B\cdot T(B)\) 的动态问题。拆分后的问题的在线或离线的属性不改变

过程:将操作按 \(B\) 为块长分块,每一块单独处理 \(T(m)\) 的静态问题(块和块前面)和 \(T(B)\) 的动态问题(块内部)。

看起来,二进制分组完全把定期重构吊着打?其实不是这样。如果解决 \(T(m)\) 的复杂度里面有和 \(m\) 无关的量,二进制分组就会因为拆分出来的问题数量过多而倒闭。

线段树分治:离线动态,删除 -> 撤销

这和多维数点问题有什么联系?没有,但是当修改对询问的贡献没有顺序要求,而且修改操作里面有删除的时候,可以将 \(m=n+q\) 个修改和询问的离线动态问题转化为 \(n\log \min(n, q)\) 个修改和 \(q\) 个询问,修改从删除变成撤销。

过程:每个修改都可能被删除,因此找出每个修改的存活区间 \([l,r]\) 表示在 \(l\) 时刻加入,\(r+1\) 时刻删除。时刻的定义比较搞笑,如果 \(n>q\) 则每两个询问之间是一个时刻,否则每两个修改之间是一个时刻,反正怎么小怎么来。将 \([l,r]\) 在线段树上拆成 \(O(\log\min(n, q))\) 个区间,把修改挂上去。最后以前序遍历线段树,走到一个线段树区间就加入在上面的修改,回溯的时候把修改撤销。在叶子节点处解决询问。

树套树:降维

如果我们坚持在多维数点问题中使用 \(\text{poly}\log\) 的树套树算法,那就必然会出现树套树。问题是选择什么树套什么树。树的类型由两部分组成:一部分是结构,例如这棵树是树状数组、线段树、平衡树之类的(原则上能树状数组就树状数组,否则根据空间需求使用线段树或平衡树);另一部分是信息,例如这棵树是维护权值的,或者是维护位置的(也就是这棵树是维护哪个维度的)。

每多套一层树,就将问题降低一个维度,代价是将问题从 \(T(n)\) 拆为了 \(\sum_{i\geq 0}n/2^i\cdot T(2^i)\)(这是线段树的情况,其它的也差不多),这些问题是同时存在的,问题的总大小将达到 \(O(n\log n)\)(这是平衡树的情况,其它的也差不多)。查询也是要从最坏 \(\sum_{2^i\leq n}T(2^i)\) 个问题(理想状态,实际数量还有个常数,线段树是 \(2\))的结构里面查询。

树套树仍然要求修改对询问的贡献是独立的。树套树可以在线处理动态问题。

莫队:离线静态 d 维 -> 离线动态 d-k 维

莫队的“维度”概念有一点难绷。描述莫队的维度,其实说的是它的 side 数量。这里我们假设每一维询问的都是一个前缀的形式,这样维度数和 side 数就一样了。

\(k\) 维莫队算法可以将离线静态 \(d\) 维问题转化为离线动态 \(d-k\) 维问题。下面具体分析一下复杂度。

  • 由于该算法复杂度是根号,比较大,因此我们先做一次离散化 \(O(m\log n)\) 把值域缩成 \(O(n)\) 方便后续分析。还要注意,我们离散化之后要保证同一维的每个数字都不同,以使得指针移动次数等于修改次数(不等于也没事,调整分块方法就行了,但感觉有点偏题)。
  • \(k=2\) 时,设第一维的块长为 \(B\),则第一维的指针移动次数为 \(O(qB)\),第二维的指针移动次数为 \(O(n^2/B)\)。所以当 \(B=n/\sqrt q\) 的时候,有最优的 \(O(n\sqrt q)\) 次修改,\(O(q)\) 次查询。
  • \(k=3\) 时,我懒得分析了反正 OI-Wiki 说是当 \(B=n/q^{1/3}\) 时有最优的 \(O(nq^{2/3})\) 次修改,\(O(q)\) 次查询。请见带修改莫队 - OI Wiki
  • \(k=4\) 时,算了不推了看这个规律我们直接总结一下。

猜测当 \(B=n/q^{1/k}\) 时有最优的 \(O(nq^{1-1/k})\) 次修改,\(O(q)\) 次查询,与 k-D Tree 的最优复杂度保持一致。\(k=1\) 时和上文的时间维转换一样,也和扫描线算法一样。

解决问题的基本算法

记号:时空复杂度

记号:\(O(X)-O(Y)\) 表示时间复杂度 \(O(X)\) 而空间复杂度 \(O(Y)\)。空间复杂度的计算大概有两类,一类是加起来(比如两个问题同时存在),一类是取 \(\max\)(比如两个问题先后存在)。所以有可能出现惊人的“时间复杂度小于空间复杂度”的情况。

在线动态零维数点

使用一个变量维护和就行了,复杂度 \(O(m)-O(1)\)。(别笑。)

在线静态一维数点

使用前缀和算法 \(O(V+m)-O(V)\) 解决问题。离散化后变成 \(O(m\log n)-O(n)\)

在线动态一维数点

如果是离线动态问题且 \(V\) 过大,那么可以离散化以 \(O(m\log n)\) 时间复杂度换取 \(V=O(n)\)。感觉是比较通用的部分,我就不写了。

树状数组可以 \(O(m\log V)-O(V)\) 解决该问题,而且常数很小,应当优先选用。动态开点可以 \(O(m\log V\cdot H(V))-O(n\log V)\) 解决该问题,其中 \(H(V)\) 是一次哈希表的复杂度。

线段树可以 \(O(V+m\log V)-O(V))\) 解决该问题,动态开点可以 \(O(m\log V)-O(n\log V)\) 解决该问题。线段树有多种写法,例如普通的递归线段树,还有 zkw 线段树,还有一种优化过的 zkw 线段树,空间不用开到 \(2\) 的次幂,可以看看 AtCoder Library 的实现。

平衡树可以 \(O(m\log m)-O(m)\) 解决该问题,有很多种平衡树:Splay、Treap、FHQ-Treap、替罪羊树、AVL、WBLT 甚至红黑树等等等等。平衡树无视值域(要默认“比较两个数是 \(O(1)\) 的”)的同时有着比较小的空间,代价是常数真的很大!关于压缩 01-Trie:我们要不把它当作平衡树来用吧。

实际上可以认为,这三种树都是把问题降维成零维数点问题,十分幽默。

分块可以 \(O(n+q(B+V/B))-O(V)\) 或者 \(O(n(B+V/B)+q)-O(V)\) 解决该问题,\(B\) 是块长。仅对于这个问题而言,显然取 \(B=\sqrt V\) 最优,时间复杂度为 \(O(n+q\sqrt V)\)\(O(n\sqrt V+q)\)

在线静态二维数点

持久化线段树是经典的解决在线静态二维数点的算法,复杂度 \(O(V+m\log V)-O(V+n\log V)\)\(O(V)\) 是因为一共有 \(V\) 个版本存在,可以离散化把 \(O(V)\) 去掉。

小波矩阵是基于归并树、分散层叠、bitset 的在线静态二维数点的算法,复杂度 \(O(m\log V)-O(n\log V/w)\)(带权时,空间复杂度上升为 \(O(n\log V)\))。资料见下文。

离线静态 k 维数点

bitset 可以用于解决离线静态 \(k\) 维数点问题(不带权),复杂度 \(O(mk\log n+nqk/w)-O(nq/w)\)。开 \(q\) 个大小为 \(n\)bitset\(b_{i,j}\) 表示是否向量 \(j\) 每一维上都偏序向量 \(i\)。枚举每一维,所有向量按照这一维排序。新开一个大小为 \(n\) 的空 bitset \(s\),按这一维的顺序,遍历到询问向量 \(i\)b[i] &= s,遍历到修改向量 \(j\)s[j] = true。这样做完 \(k\) 轮以后,就知道向量之间是否偏序。

空间太大了。对序列分块,块长为 \(B\),做 \(q/B\) 轮,每轮的问题变成 \(n\) 个修改 \(B\) 个查询。复杂度变为 \(O(mk\log n+nqk/w)-O(nB/w)\)(注意隐含条件 \(B\geq w\))。没错,由于排序结果是固定的,所以时间复杂度不变,但是空间复杂度会凭空下降。实践中 \(B\) 能取多大是多大。

参考资料是 P3810 【模板】三维偏序(陌上花开)(bitset) - 洛谷专栏题解:P3810 【模板】三维偏序(陌上花开) - 洛谷专栏。还看到一个课件 - FHR-分块bitset求高维偏序,我感觉这个课件里面讲的方法有点唐,我就不写在这里了。

在线静态 k 维数点

k-D Tree 是经典的解决在线静态 \(k\) 维数点的算法,复杂度 \(O(n\log n+qn^{1-1/k})-O(n)\)。著名的数据结构专家李欣隆告诉我们:k-D Tree 将暂时没有复辟的可能。据 UOJ 群友研究,k-D Tree 的查询常数 \(\geq 10\),所以要谨慎使用。

在线动态 k 维数点

问题比较具体时,可以考虑分块算法,这里就不说了,因为分支实在是太多,研究不过来。

一个分块的例子是 P3810 【模板】三维偏序(陌上花开)题解 - 洛谷专栏

具体的例子:离线静态三维数点(三维偏序)

  • 离线静态三维数点 ->(时间维)离线动态二维数点 ->(cdq 分治)离线静态二维数点->(时间维)离线动态一维数点 -> 树状数组
  • 离线静态三维数点 -> bitset 或 3-D Tree
  • 离线静态三维数点 ->(时间维)离线动态二维数点 ->(二进制分组)离线静态二维数点 -> 2-D Tree
  • 离线静态三维数点 ->(时间维)离线动态二维数点 ->(树套树)离线动态一维数点 -> 树状数组
  • 离线静态三维数点 ->(二维莫队)离线动态一维数点(\(n\sqrt n\) 个修改,\(n\) 个询问)-> 分块(单次修改 \(O(1)\),单次询问 \(O(\sqrt n)\),总复杂度 \(O(n^{3/2})\)

以上就是这个问题的几种主流做法的思路,第一种 cdq 分治 + 树状数组的应当是最快的。

带二分的问题是怎么一回事?

简述

我们时常见到的问题——“在线静态区间第 \(k\) 小”其实可以被描述为二分问题,即我们需要在某一个维度上进行二分,最后二分得到的值才是答案。这里,询问的从一个向量变成了少一个维度的向量和一个值,回答的从一个值变成了向量缺失的那一维。显然,我们可以将一个这样的二分问题直接转化为 \(O(\log V)\) 个正常的在线多维数点问题——直接二分即可。注意无论原问题是否在线,我们这样做都会让它变成在线的(原问题离线时,我们这样做其实是让它变成 \(\log V\) 轮与原规模一样的离线问题,其实也是一种在线),而且每次询问的向量都只有一个维度不一样,我们这样转化问题是严重加强了这个问题,其中的优化空间很大。

应该要有一个分散层叠算法的介绍写在这里吗?但是我对此没有太多研究,所以只能送大家去读分散层叠算法学习笔记 - 洛谷专栏《浅谈利用分散层叠算法对经典分块问题的优化》 - 学习笔记 - p_b_p_b - 博客园等博客了。

针对每次询问向量的相同维度优化:持久化线段树和小波矩阵

以“在线静态区间第 \(k\) 小”为例,我们可以用持久化线段树(有人叫它主席树)和小波矩阵优化这个问题的复杂度,达到 \(O(m\log n)-O(n\log n)\)(持久化线段树)和 \(O(m\log V)-O(n\log V/w)\)(小波矩阵,可以自行离散化)的复杂度。两个算法的想法比较相似——都是针对每次询问向量的相同维度优化;但又有不同——具体优化的方法是不同的。

持久化线段树:思路是设置 \(n\) 个版本,第 \(i\) 个版本里面有 \(a[1..i]\) 的信息,然后建立的是值域线段树。询问 \([l,r]\) 区间第 \(k\) 小值的时候,直接用版本 \(r\) 的线段树减去版本 \(l-1\) 的线段树,这样得到了一棵有 \(a[l..r]\) 信息的值域线段树,在上面二分即可。

小波矩阵:从归并树出发,对 \(a[1..n]\) 建立一个不是归并树但是比较像的东西,它像一棵满的值域线段树,树上的区间 \([L,R]\) 维护的是保留值在 \([L,R]\) 之间的 \(a[1..n]\) 序列。询问 \([l,r]\) 区间第 \(k\) 小值的时候,从树的根出发向儿子走,每次询问左儿子的序列里面对应原序列 \([l,r]\) 区间里有多少个值,从而决定向哪边走。如果直接二分找到对应关系,就是 \(O(\log^2n)\) 的。考虑分散层叠的思想(但不完全是),在当前节点上,由于当前节点是左右儿子合并上来的,我们可以记录当前节点的序列上的一个数在左右儿子那里分别对应哪个位置(飞指针),这样我们就可以在当前节点以 \(O(1)\) 的代价把对应区间传到左右儿子,于是只需要做前缀和,复杂度降低为 \(O(\log n)\)。后面还有一步压空间的,也比较巧妙,请见浅谈 Wavelet Matrix - 洛谷专栏详细揭秘:如何发明小波矩阵 - 洛谷专栏等资料。

整体二分:离线 d+1 维二分问题 -> 离线 d 维数点问题

如果修改对询问的贡献是独立的,同时询问的答案具有可二分性,那么可以尝试整体二分。

  1. 假设当前二分的那个缺失维度的范围是 \([L,R]\),然后有一个操作序列,有修改也有查询。递归保证了所有修改在缺失维上的值在 \([L,R]\) 范围内。
  2. 递归边界 \(L=R\) 或者操作序列为空先判掉。然后令 \(mid=(L+R)/2\)
  3. 按顺序扫描操作。对于修改,只保留缺失维 \(\leq mid\) 的修改,并把缺失维删掉。对于询问,将缺失维删掉,发起一次数点询问。(这里就降维了)
  4. 分两类往下递归。对于修改,缺失维 \(\leq mid\) 的分左边,否则分右边。对于询问,将发起的数点询问的回答和目标值比较,如果应该走到左边,就走到左边,否则把目标值减掉这次回答(削除左边的影响;也有不用的情况,具体问题具体分析),走到右边。然后向下递归。

拆分的问题数量和大小不太好写,因为这个结构只保证深度为 \(O(\log V)\),也就是问题大小总和为 \(O(m\log V)\),每次处理的问题大小不超过 \(O(m)\)(因此它空间复杂度比持久化线段树好)。要强行写的话可能可以写成 \(T(m)\log V\),当 \(T(x)+T(y)\leq T(x+y)\) 时。然后整体二分实际上是在做转化,将二分问题转为数点问题,问题规模如上,维数降一维(被二分的那一维被删掉)。另外,整体二分要求离线。

有一道题,留在这里:P3527 POI 2011 MET-Meteors - 洛谷

posted @ 2026-01-30 11:47  caijianhong  阅读(116)  评论(0)    收藏  举报