【算法证明 四】摊还分析

按照算法导论的顺序,下面该介绍数据结构,动态规划,贪心算法了。但是基本的数据结构比较简单,靠数学直觉也能很好的理解,就不总结了。动规和贪心是那种听懂很简单,用起来很难得算法,准备放在后面总结。
摊还分析对一般人来说是比较新奇得技术,应该比较少的人会注意到这类问题,而且与图相关的算法复杂度分析经常遇到需要摊还分析的情况。所以接下来两节准备总结下摊还分析的技术和思考逻辑。并先用该技术来证明并查集的算法复杂度。

0 摊还分析

摊还分析基于这样一个观察:对一个数据结构,一半有多种操作方式,实际应用中,可能有更多组合操作。例如:栈有:入栈(A) ,有出栈(B),实际应用中可能还会定义一次出栈n个元素©这种操作。当程序中随机(实际并不随机,杂乱比较好一些)的运行这些操作时,会生成一个操作序列。这个操作序列的总体复杂度是多少?平均每个操作的复杂度又是多少?

这种情况下最坏复杂度不可以通过 单个操作最坏复杂度 x n 这种粗暴算法来计算,因为操作之间有一个隐形的约束。如果对上面的例子中,操作 C 有一次 pop 出 k 个元素,那么用 O ( k n ) O(kn) O(kn) 来计算整体复杂度显然是不合理的。
设栈中的元素的个数为 入栈操作 的个数 q。则 pop 也最多pop出 q 次。无论你一次性 pop 出几个,总共只能 pop q 次。所以总体复杂度是 O ( 2 q ) O(2q) O(2q),由于 q ≤ n q \le n qn,不妨视为 O ( n ) O(n) O(n),摊还复杂度是 O ( 1 ) O(1) O(1)

为了解决这类问题,设计了三种证明思路,分别是:聚合分析、核算法、势能法。其中核算法是一个很不好的名字,不知道哪个大聪明起的。它英文是:accounting method。所以应该是:『核算』法,而不是『核』算法。其实三种证明思路是相似的,哪个好用用哪个。

1 聚合分析

聚合分析的思路是,一个长度为 n n n 的操作序列最坏情况下总花费时间为 T ( n ) T(n) T(n),然后得到每个操作的摊还代价为 T ( n ) / n T(n)/n T(n)/n。上面的栈序列操作用的就是这种思路。该思路比较直白,就是一个计算。作为该类思路的一个练习,考虑下面的问题:

练习:二进制计数模拟

一个 k 位 2 进制数。当对其进行 + 1操作时,要从低位向高位遍历,直到遇到一个0,才可以 + 1结束。其他情况下考虑进位,需要对该位进行翻转。所以一次+1操作的复杂度,跟当前数字末尾的1的个数有关。最坏情况下,全部需要反转。但总情况和平均情况呢?可以通过简单模拟找出翻转规律来计算总的复杂度为 O ( n ) O(n) O(n),摊还复杂度为 O ( 1 ) O(1) O(1)

2『核算』法

核算法是先给一个猜测的摊还代价,然后证明这个猜测正确。例如:我想证明摊还代价为 O(n) 的,我们不妨设每个操作的摊还代价 c ^ i = 10 \hat c_i= 10 c^i=10,然后让他跟真是的代价 c i c_i ci进行对比。多的代价可以存起来,少的代价用之前存起来的补,总之,只要其满足下面的不等式即可
∑ i = 1 n c ^ i ≥ ∑ i = 1 n c i \sum_{i=1}^{n}\hat c_i \ge\sum_{i=1}^{n} c_i i=1nc^ii=1nci
即在序列的每一个位置,我猜测的代价都足够满足真实的代价。

3 势能法

对于核算法,我们猜测了一个操作的摊还代价。但是这是不准确的。势能法将猜测的摊还代价比实际代价多出来的部分量化了。
设数据结构的初始状态为 D 0 D_0 D0,经过操作 i i i后,数据结构的状态会从 D i − 1 D_{i-1} Di1变换到 D i D_i Di。也就是说我们将操作 i i i引起的数据结构状态也考虑进去了。为了量化这个状态,我们定义势能函数 Φ ( D i ) \Phi(D_i) Φ(Di)。势能法的摊还代价可以定义为:
c ^ i = c i + Φ ( D i ) − Φ ( D i − 1 ) \hat c_i= c_i + \Phi(D_i) - \Phi(D_{i-1}) c^i=ci+Φ(Di)Φ(Di1)
Φ ( D i ) − Φ ( D i − 1 ) \Phi(D_i) - \Phi(D_{i-1}) Φ(Di)Φ(Di1)直接量化了摊还代价与实际代价的差异。所以代换到核算法的公式中,我们可以得到,只要找到势能公式满足
∑ i = 1 n [ Φ ( D i ) − Φ ( D i − 1 ) ] = Φ ( D n ) − Φ ( D 0 ) ≥ 0 即可 \sum_{i=1}^{n}[\Phi(D_i) - \Phi(D_{i-1})] = \Phi(D_n)-\Phi(D_0) \ge 0 即可 i=1n[Φ(Di)Φ(Di1)]=Φ(Dn)Φ(D0)0即可
所以,势能法与核算法的证明逻辑是一样的,只是方式有些差异,形式上更简单。但是势能函数的选取很考验技巧,要满足两点:

  1. 势能公式能够便于计算摊还代价
  2. 势能公式能够满足你需要的算法复杂度要求

以二进制计算算法为例,使用势能法证明如下:
选取势能公式为 Φ ( D i ) = 二进制中 1 的数量 b i \Phi(D_i)=二进制中 1 的数量b_i Φ(Di)=二进制中1的数量bi。考虑 b i − b i − 1 b_i-b_{i-1} bibi1可能的值。算法把 D i − 1 D_{i-1} Di1中末尾的1全部设为0,并把下一位设为1。设末尾的1为 t i t_i ti个,则 b i − b i − 1 = − t i − 1 + 1 b_i-b_{i-1} = -t_{i-1} + 1 bibi1=ti1+1。因此:
c ^ i = c i + Φ ( D i ) − Φ ( D i − 1 ) = t i − 1 + 1 + − t i − 1 + 1 = 2 \hat c_i= c_i + \Phi(D_i) - \Phi(D_{i-1})=t_{i-1}+1+ -t_{i-1} + 1 = 2 c^i=ci+Φ(Di)Φ(Di1)=ti1+1+ti1+1=2
所以摊还复杂度为 O ( 1 ) O(1) O(1)
再考虑势能函数满足要求:
如果二进制计算从 D 0 D_0 D0开始,那么总会有 b i ≥ 0 b_i \ge 0 bi0,即$ Φ ( D i ) ≥ Φ ( D 0 ) \Phi(D_i)\ge\Phi(D_0) Φ(Di)Φ(D0)。证明完毕

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值