首发于算法随笔

珂朵莉树的复杂度分析

(副标题:珂朵莉树为什么可以做到线性复杂度)

我偶然间看到了珂朵莉树这个数据结构并对它的复杂度感到好奇。在网上搜了一下没找到完整的证明,倒是看到了不少不正确的伪证...所以在这里写一下。

注:cf的editorial以及评论区里有不完整的证明,已经包含了主要的技术细节,只是缺个总结整理。但是这部分没完全写清楚的话可能比较容易被误读吧。

以下以珂朵莉树的起源题cf 896C为例分析它的复杂度。为了简单,这里把原题中的操作3和4替换为出题人lxl @RhmBWT 在editorial中提到的简化版操作(区间求和)。分析技巧对原问题中的操作也是适用的。(这个简化版用线段树也是能做的,但不是重点)

问题:设计一个数据结构维护一个长度为n的数组,并支持如下操作:
1. add:将区间[l,r]内的值+x。
2. cover:将区间[l,r]内的值设为x。
3. sum:求区间[l,r]内的值的和。
题目保证操作的类型以及l和r在范围内独立均匀随机。

注:原题的随机数生成器较弱,例如事实上833333337是随机种子迭代函数的一个不动点。下文在理想随机数的前提下进行讨论。

算法:珂朵莉树将数组分成包含连续相同值的若干段(下文称为基本段),并使用set或链表维护。对区间 [l,r] 进行操作时,暴力找出区间内的所有基本段并对每段花O(1)的时间进行询问或修改。令 O(\mathrm{pred}) 表示找到[l,r]区间内第一个基本段所需的时间(因为这一步一般使用predecessor search),并令d表示区间内当前包含的基本段数。可以看出,每个操作均最多花费 O(\mathrm{pred}+d) 的时间(根据具体实现,操作2的复杂度可以与d无关),此外操作1会使总基本段数增加O(1),操作2会使总段数减少O(d-1),操作3不影响总段数。


Lemma 1. 若进行了m次操作,令随机变量 d_i 表示第i次操作包含的基本段数,则 \mathbb{E}[\sum_{i=1}^m d_i]=O(n+m) 。这也表明在除去predecessor search部分的时间后,总时间复杂度的期望为 O(n+m)

Proof. 令随机变量 f_i 表示第i次操作删除的基本段数。在固定前i-1次操作以及第i次操作的左右端点l和r的情况下, \mathbb{E}[d_i]=O(\mathbb{E}[f_i]+3) (因为操作类型是均匀随机的,并与我们固定的部分独立)。而我们知道 \sum_{i=1}^m f_i=O(n+m) ,因为初始有 O(n) 个基本段,而每次操作最多增加 O(1) 个基本段,一共删除的基本段数不会超过总共增加的基本段数。最后对固定部分的所有可能性以及所有i求和,利用期望的线性性即证。

注:这个结论在cf的editorial里已经证了,不过没有写得那么formal。事实上这个在直觉上是很对的,不过可能很多人脑海中的证明并不那么严谨。


Lemma 2.p_j 表示一个下标为 j\in\{1,\dots,n\} 的基本段的端点在经过了m次操作2后仍然为基本段的端点的概率。那么有 \frac{1}{n}\sum_{j=1}^n p_j=O(\frac{1}{m}) 。(这里对于这个端点先消失后重新出现的情况,不算作“仍然为”。)

Proof. 下面是从cf的评论区里搬过来的@Akababa 的证明:

对于一个下标为 j 的端点,若它在m次操作2后仍然存在,则所有操作2的区间都没有覆盖该端点(这里考虑的是上界,所以可以忽略其余的操作1和3)。对于每次操作,没有覆盖该端点的概率为\frac{(j-1)^2+(n-j)^2}{n^2} ,对所有操作均成立则取m次方。我们有 \frac{1}{n}\sum_{j=1}^n p_j=\frac{1}{n}\sum_{j=1}^n\left(\frac{(j-1)^2+(n-j)^2}{n^2}\right)^m 。现在令x=\frac{j}{n},则 p_j\leq (x^2+(1-x)^2)^m\frac{1}{n}\sum_{j=1}^n p_j\leq \int_{0}^1(x^2+(1-x)^2)^m{\rm{d}}x ,用Mathematica或者手动积分可知答案的上界为 O(\frac{1}{m})

这里有一些微妙的问题值得注意。以下是一些繁琐的技术细节,可以跳过:

求导可得f(x) 是关于 x=\frac{1}{2} 左右对称的下凸函数,所以是先减后增的,这也是上述证明中可以用积分作为 \frac{1}{n}\sum_{j=1}^n p_j 求和式的上界的原因。但注意这个上界当 m\gg n 时是不紧的:这是因为当 m\gg n 时被积函数 f(x)=(x^2+(1-x)^2)^m 是非常病态的,曲线下方面积集中在两端,所以用积分近似求和会产生较大的误差。我用几何画板画了个图:

并用Mathematica算了一下,例如 n=100m=1000\int_0^1(x^2+(1-x)^2)^m{\rm{d}} x\approx 0.00100 ,而 \frac{1}{n}\sum_{j=1}^n\left(\frac{(j-1)^2+(n-j)^2}{n^2}\right)^m \approx 3.7275\times 10^{-11} 非常小。若把 n 替换为 m ,则\frac{1}{m}\sum_{j=1}^m\left(\frac{(j-1)^2+(m-j)^2}{m^2}\right)^m\approx 0.00031,与上界 O(\frac{1}{m}) 相近。

事实上当 m\geq 10n\log n 时,考虑 \sum_{j=1}^n p_j 和式中单调递减的第 1\sim \frac{n}{2} 项,其中第一项和第二项的比值为 \left(\frac{(n-1)^2}{1^2+(n-2)^2}\right)^m =e^{\Omega(\log n)}=\Omega(n) ,所以第一项的值有总和的 \Omega(1) 倍那么大,可以用第一项的值近似总和。此外, \sum_{m=10n\log n}^\infty \left(\frac{(n-1)^2}{n^2}\right)^m \leq \frac{1}{n} ,因为这是一个几何级数。

简单地说,当 m 远大于 n 时一个基本段的端点在经过了m次操作2后仍然存在的概率是快速减小的,所以在Lemma 3的结论中可以用 O(\log\min\{m,n\}) 替代 O(\log m) 。这是附加的这部分分析的目的。


Lemma 3.t 表示总基本段数,则m次操作后 t 的期望为 O(\frac{n}{m}+\log \min\{m,n\}) 。特别地,当 m=\Theta(n) 时,m次操作过程中 t 的期望平均值为 O(\log n)

Proof. 这是Lemma 2的一个简单推论,但我没看到有人正式写过。要求总基本段数,只需求出基本段的端点总数,而每个端点可以追溯到它第一次被某个操作创建时的时刻。换句话说,只需在每个端点被创建的时刻算出它到最后仍然存在的概率即可。

初始时有 O(n) 个端点,根据Lemma 2,平均下来每个端点到最后仍然存在的概率为 O(\frac{1}{m}) (直觉上说, m 个操作中会有 \Theta(m) 个操作2;严格地说,令r.v. k 表示m个操作中操作2的数量,则 \mathbb{E}[k]=\Theta(m) ,根据Chernoff bound, \mathrm{Pr}[k=\Theta(m)] \geq 1-e^{-\Theta(m)} 。对 k 的大小分类讨论求和可得,端点在m次操作后仍然存在的概率 \leq O(\frac{1}{m})+e^{-\Theta(m)}\cdot 1 =O(\frac{1}{m}) 。注意这里用到了随机变量之间的独立性)。每个操作会创建 O(1) 个端点,考虑倒数第 i 个操作,创建的端点到最后仍然存在的概率为 O(\frac{1}{i}) (因为端点的位置是均匀随机的)。求和可得 \mathbb{E}[t]=O(n\cdot \frac{1}{m}+\sum_{i=1}^m\frac{1}{i}) =O(\frac{n}{m}+\log m) 。(根据Lemma 2的补充部分, m\gg n 时更精确的上界为 O(\log n) 而不是 O(\log m)t 稳定时的大小与 m 无关)。特别地,令 t_i 表示第 i 次操作后的 t 值,则 \mathbb{E}[\frac{1}{n}\sum_{i=1}^n t_i] =\frac{1}{n}\cdot \sum_{i=1}^nO(\frac{n}{i}+\log i) =O(\log n)

这和这篇文章以及这篇描述的实验现象是吻合的(虽然前一篇的理论分析有点问题)。


接下来只需分析predecessor search的复杂度。如果用set实现,则O(\mathrm{pred})=O(\log t) ,根据 \log x 的上凸性以及Jensen不等式,n次操作的期望总时间为 O(n\log\log n) 。如果用链表实现,并暴力做predecessor search,则 O(\mathrm{pred})=O(t) ,n次操作的总时间为 O(n\log n)。(虽然因为常数问题,实践中两者的速度五五开。)

注:注意一个细节,用set实现的话初始化时要一次性构造整个set复杂度才是对的。如果分 n 次insert的话复杂度是 O(n\log n) ,不过实际上速度差不太多。

下面介绍怎么把总复杂度做到 O(n+m) 。对于前 m=O(\frac{n}{\log n}) 个操作,用set维护基本段,predecessor search的总复杂度为 O(\frac{n}{\log n})\cdot O(\log n)=O(n) ,并注意set删除一段元素的复杂度是正比于删除元素个数的。对于之后的序号为 \Omega(\frac{n}{\log n}) 的那些操作, t 的平均值为 O(\log n) 。然后使用Mihai Pătraşcu和Thorup的一个强大的结论[1],可以维护 t 个元素的dynamic integer set,并在 O(\frac{\log t}{\log w}) 的时间内支持insert, delete, pred操作,其中 w\geq\log n 为字长。根据Jensen不等式,insert, delete, pred操作的平均复杂度为 O(\lceil\frac{\log \log n}{\log w}\rceil)=O(1) 。所以总复杂度是线性的。

一点题外话:Pătraşcu已经不在了,好可惜啊。我最近还引用了他的好多work来着,每次看着他名字上的方框就挺伤心。


最后简单提一下我见过的一些伪证:

1. Lemma 1在直觉上很显然,但是很多证明是不严谨的,处理不当的话会碰到一些随机变量之间的独立性问题。

2. 很多证明提到了 t 的平均值为 O(\log n) ,然后认为自始至终 t 的值均为 O(\log n) ,这事实上是违背Lemma 3的。

3. 这里这里t 的平均值为 O(\log n)的证明是有偏差的。在“平均”情况下,如果基本段是均匀分布的,那么每次操作2期望会删掉 \Omega(t)=\Omega(\log n) 个基本段。但事实上这是不对的(违背了均摊下来每次只会删掉 O(1) 个基本段的事实),因为一些位置靠近1或者n的较短基本段是很难被删掉的。另外对操作1复杂度的分析也不是紧的。不过跑了实验还是值得表扬。

另外cf上的原证明也有一些细节问题需要注意,当然idea是好的。

Conclusion

  1. set实现的珂朵莉树的复杂度为O(n\log\log n),而链表实现的复杂度为O(n\log n)。理论复杂度可以做到 O(n) ,但没必要。

2. 因为复杂度是均摊的,以及 t 减小的速度并不太快,所以可能很难可持久化。

3. 有兴趣的同学可以算算运行时间的方差,留作习题。

References

[1] Patrascu M, Thorup M. Dynamic integer sets with optimal rank, select, and predecessor search[C]//2014 IEEE 55th Annual Symposium on Foundations of Computer Science. IEEE, 2014: 166-175.

编辑于 01-19

文章被以下专栏收录