数据结构与算法复习——5、摊还分析入门

5、摊还分析入门——斜堆分析

  这一篇来介绍摊还分析。摊还分析听上去像是平均分析,然而却不是那么严格。在介绍摊还分析之前,先来讲一个故事:

设两只小猫在相距1000米处相向而行,速度均为10m/s。两者的母亲从中心开始向某只小猫以50m/s的速度跑去,相遇后立刻掉头向另一只小猫跑去,速度仍是50m/s,如此往返直到小猫相遇。求猫妈妈跑过的总路程。

可见猫的速度非常快。如果我们按部就班地分析,可能要分析到猴年马月了。这里不妨引出一个额外的变量:时间。则解答如下:

设总过程的时间是$t$,那么可求得$t = \frac{1000}{10+10} = 50s$,则猫妈妈跑过的路程为$s = vt = 2500m$。

可见十分简单。

均摊分析就是这样,直接去求每个操作的平均耗时显得非常困难,我们引入一个额外的可能会影响时间的值:位势函数$\phi$,来帮助我们分析。

一、前置知识:斜堆

  这一篇我们利用摊还分析来分析斜堆的合并操作的均摊复杂度。关于斜堆,它来自于经典的左偏树。两者在我以前的博客https://www.cnblogs.com/halifuda/p/8319053.html里都有简单介绍。另外,博客里对于$d-$堆合并操作复杂度的分析有错误,通过上一篇我们已经知道,二叉堆可以以$O(N)$的复杂度合并,只需要重新建堆即可。

二、位势函数

  摊还分析一个数据结构的时间复杂度,关键点在于寻找到位势函数。一般来说,位势函数的值是由某一刻(即某两次操作之间)整个数据结构的状态决定的。我们希望位势函数有这样一些特点:

1、它是描述均摊时间的函数。譬如,对数据结构$R$,执行一次操作$P$,所需要的实际时间是$T$,而$P$还额外造成了一些位势的变化$\Delta \phi$,从而进行了$N$次操作$P$后,所需要的均摊代价便是$\sum_{i=1}^{N} T_i + (\phi_N - \phi_0)$。我们希望最终的这个代价是时间的一个上界,但优秀到可以接受。当然,位势函数一般不需要记录在数据结构里,它只是我们分析的手段。

2、从另一个方向理解,$R$的某个状态下,位势$\phi$越高,进行一次操作的所需实际时间越多;反之则越少。这样,位势便像是一个可以使用的“储蓄”。如果均摊时间是$T^*$,当一次操作的花费$T < T^*$时,它可能造成了位势的增加:$\Delta \phi > 0$;当一次操作的花费$T > T^*$时,它可能反而减小了位势:$\Delta \phi < 0$。这样,虽然花费高的操作花费了更多时间,但它减少了位势,以便后续出现更多的花费低廉的操作,使得均摊时间不会太劣。从而,我们对均摊时间的分析就是:

$T + \Delta \phi = T^*$

3、再次考察1中的和式:

$\sum_{i=1}^{N}T_i + (\phi_N - \phi_0) = N T^*$

它是求均摊时间的严谨方法,因为通过$T + \Delta \phi = T^*$来求单次操作的均摊时间并不那么严谨。当然,如果位势函数很好的话,这样做也是没问题的。不过我们先来关注位势函数成立的一个基本条件,那就是$\phi_N - \phi_0 \geq 0$。从和式可以知道这一条件的意义:均摊时间必须是总时间的上界,因此均摊时间和至少要大于等于总时间和。从而,位势函数最好满足:任意时刻都是正的(至少也要非负)、$\phi_0 = 0$。

4、3中提到,用$T + \Delta \phi = T^*$求单次均摊时间不太严谨,这是因为如果位势函数不够好,等式右边可能不是一个很好的结果,使得我们很难看出实际的均摊时间。但是,如果位势函数足够好的话,等式右边保持稳定(譬如是一个常数,或者能够证明一个上界),则等式也可以分析出操作的均摊复杂度。这样,一个好的位势函数应该能够在等式里消去$T$中的不确定项。

这些就是位势函数的选择的基本要求。总结一下就是:由整个数据结构的状态规定、任意时刻都是正的、尽量消去时间操作函数里的不确定项。再次声明。任意“时刻”是指任意两个操作之间。

三、斜堆分析

  由于斜堆的主体操作就是合并,因此下面我们只对斜堆的合并操作进行分析。斜堆要用上摊还分析的原因,是因为我们期待它的操作能够像二叉堆一样达到单次$O(\log N)$,但是我们又没办法确定(并且可以肯定有些操作会超过$O(\log N)$的),因此摊还分析是一个好想法。既然要做摊还分析,就必须先选好一个合适的位势函数。

我们知道,两个斜堆的合并总是在它们的右路径上解决,合并结束之后,这些右路径上的结点又都交换左右子树,最终导致它们都去到左路径上去了。

一个自然的想法是,右路径的结点数作为位势函数。可是如果我们这样做,会导致我们算不出来$\Delta \phi$,因为新斜堆的右路径是什么样的我们不清楚,我们只知道左路径的样子。如果我们令右路径减去左路径为位势函数,则难以保证非负性。

如果用右结点的个数做位势函数呢?尝试来分析。每次合并斜堆$H_1$和$H_2$,总会有一个作为新根,另一个出现在右路径上,当最后合并结束,又交换到左边。可以知道原本右路径上的右结点都变成左结点了,而右路径上这些点的(可能的)左子结点都变成右结点了,问题在于,我们并不知道右路径上这些点曾经是否包含左子结点。这里我们就遇到了与上一个想法同样的问题。

一个好的想法是这样的:按照结点的轻重来划分。其中,轻结点指左子树的大小大于右子树的结点,重结点反之。我们用重结点的个数来做位势函数。它的好处在于:

1、$\phi_0 = 0$,总是非负的,而且一个坏的合并必然会在右路径上遇到很多重结点,反之亦然,这使得它很适合作为一个位势函数;

2、它避免了刚刚遇到的问题,因为一个重结点在合并时必然还是重结点(因为合并会在它的右路径上进行),而最后交换时就必然变成轻结点了。我们不知道轻结点交换后会变成什么样,为了去求上界,我们假设最坏的情况,即它们变成重结点了,这样,求不出位势函数变化的问题就解决了。

请注意,这里做的假设放在刚刚是不可行的,因为如果假设它们都有左子结点,那么右结点的个数根本不会减少,位势函数单增使得它已经不适合作为位势函数了。

下面,我们就用这个位势函数来求斜堆合并操作的均摊时间上界:

假设合并斜堆$H_1$、$H_2$时,它们右路径上分别有$l_1$、$l_2$个轻结点,$h_1$、$h_2$个重结点,各自共有$l_1+h_1$、$l_2+h_2$个结点。省去大$O$,我们有:

$T = l_1+h_1+l_2+h_2$

由于只有右路径上的结点会被交换左右子树,从而轻重性可能改变,而经过刚刚的分析,重结点总会变成轻结点;为了求上界,我们令新的重结点尽可能多,也即轻结点都变成了重结点,这样,位势函数的变化即为:

$\Delta \phi = l_1 + l_2 - h_1 - h_2$

从而就有:

$T^* = 2(l_1+l_2)$

现在我们要证明$T^*$的上界。实际上,轻结点有如下特征:若一棵$N$个结点的树$R$是轻的,则它的右子树的大小小于$N/2$。一个斜堆的右路径指的是:从根一直通向右子树。因此在一个斜堆的右路径上:当一个结点是轻结点,那么接下来的右子树的大小至少会减半。如果右路径上每个结点都是轻结点,那么数量也不会超过$\log N$。就算其中有一些重结点,最坏的情况莫过于在遇到下一个轻结点之前,所有点都在右子树上,右路径上右子树的大小不会变大,从而轻结点的个数不会因为这些出现在中间的重结点而变得更多。由于刚刚证明了均摊时间只与轻结点个数有关,这些重结点不在我们的考虑范围之内,我们就证明了:

$T^* = O(\log |H_1| + \log |H_2|) = O(\log N)$

从而斜堆的一次合并操作拥有$O(\log N)$的均摊时间复杂度。由于插入、删除等操作都是一些合并,因此它们的单次时间复杂度也是$O(\log N)$。

 

  摊还分析的方法大抵如此。可以见到,找到合适的位势函数是最困难的部分。我们还见到,位势函数不一定非常优秀,完全被使用,只要能求出$T^*$的上界即可。实际上斜堆的分析已经不太简单,由于摊还分析的复杂性,我将再举出一些例子。为此,我会先介绍一些数据结构。

posted @ 2021-02-03 10:52  Halifuda  阅读(412)  评论(0编辑  收藏  举报