[算法Tutorial]Amortized Analysis,平摊分析

对于一个操作的序列来讲,平摊分析得出的是在特定问题中这个序列下每个操作的平摊开销。

一个操作序列中,可能存在一、两个开销比较大的操作,在一般地分析下,如果割裂了各个操作的相关性或忽视问题的具体条件,那么操作序列的开销分析结果就可能会不够紧确,导致对于操作序列的性能做出不准确的判断。用平摊分析就可以得出更好的、更有实践指导意义的结果。因为这个操作序列中各个操作可能会是相互制约的,所以开销很大的那一两个操作,在操作序列总开销中的贡献也会被削弱和限制。所以最终会发现,对于序列来讲,每个操作平摊的开销是比较小的。

换句话说,对于一个操作序列来讲,平摊分析得出的是这个序列下每个操作的平摊开销。

基本公式为 amortized cost = actual cost + accounting cost,我们记为$\hat{c}_i = c_i + a_i$

这里,$\forall n, \sum_{i=0}^{n}a_i \geq 0$

 


 

1. Stack

这里的操作有push,pop和multipop($k$) —— 弹出栈顶的$k$个元素,显然,直观上来说,push和简单pop的操作都是高效的,即其性能都在$O(1)$的样子,也就是说,他们的Actual Cost都是1。

而multipop则不同,它每次可能会要求弹出至多$n$个元素,如果进行长度为n的操作序列都进行这样的操作,那么最坏情况下的复杂度可以达到$O(n^2)$。这是一个不太紧的上界。

我们发现,由于栈中每次pop元素都是建立在push的操作次数的基础上,所有的pop操作受到push操作的限制。每个元素进栈的同时,都隐含着一次出栈的“未来”,所以,push操作最多能够push共$O(n)$个元素,那么pop的次数也至多为$O(n)$。而每个push、pop的代价均为$O(1)$,所以整个栈上的数据操作的复杂度上界可以是$O(n)$!

说得生动而形象一点,我们用上面的公式来记账。

 

操作Amortized CostActual CostAccounting Cost
push211
pop01-1
multipop0$min(k, s)$,$s$为当时栈中的元素个数。-$min(k, s)$

 

用钱来说,因为Amortized本身有“分期偿还”的意思呀~

假设你每次在进行push操作的时候,都花2块钱,那么你在push的时候,只花了1块,还有1块钱就作为存款了。每当你进行pop操作,或者是multipop操作,我们不再需要花钱啦!!每次出栈一个元素就从存款中扣掉1块钱,而每时每刻栈中剩余元素个数都大于等于0的啦,所以存款的总和不会为负,$\forall n, \sum_{i=0}^{n}a_i \geq 0$ 自然成立。

  


 

 2.Doubling Array


就是一开始分配一定量的内存A,如果在向这个内存空间插入对象时发现空间不足的话,就重新分配一块比原来大的内存B,我们就认为$size(B)=2size(A)$,把原内存A中所有的对象都复制到内存B中,这时候的代价为$O(n)$然后把新加入的对象增插入到B的空余位置中,最后把内存A释放掉。呵呵,记得内存泄漏!!

乍一想,可能每次插入都可能要再malloc一块空间,代价就是$O(n)$啊,那么$n$次操作复杂度就是$O(n^2)$啊!So terrible!!

好了,平摊分析会告诉你上界是$O(n)$

我们假设一开始这个表是空的,然后向这个表中依次插入n个元素,想一想,就会发现,这个操作序列中是不可能连续$n$次出现最坏性能的(怎么可能每次插入都要double空间呢?),甚至说,出现最坏性能的机会是比较小的,仅在第2的幂的数。除此以外,直接插入就是了,不需要额外申请空间,代价仅仅为$O(1)$。

所以,我们有Actual Cost

$$c_i = \left\{
\begin{array}{ll}
1 & i \neq 2^x,x \in N \\
i+1 & i=2^x,x \in N
\end{array}
\right.$$

直接对$c_i$求和,我们可以得到
$$\sum_{i=0}^{n} c_i=n+\sum_{i=0}^{\log n} 2^i \leq n + 2n = 3n$$
可见,如果平摊的话,每个操作的平均cost为3!所以这样的一个操作,上界是$O(n)$!

那么为什么不是2?我们助教也是很认真负责哒~如果每个数插入的时候,插入花了1,那么在一次double的时候,另一块钱就被用完了,以后插入要double的时候,就没有存款啦!所以,当代价为3的时候,还有两块钱的存款,一块钱用于double时自己的花费,另一块钱就是给它前面的所有的已经没有存款的元素用啦~~好心人呐!!!

  


 

 3. Binary Counting

这是一个二进制计数器,用一个$k$-bit的数进行计数,就像当年数字逻辑电路的面包板上的那个玩意儿...基本的置位set操作和复位reset操作本身的代价都为$O(1)$。

直观上,最坏情况每个bit位都要发生改变,代价就是$O(k)$,$n$次计数操作就是$O(kn)$。

可是,平摊分析告诉你,上界是$O(n)$,你应该已经习以为常了吧~~
假设一个8-bit的数从高位到低位分别存入数组$A[7..0]$中用于计数,那么每次计数加1,最低位$A[0]$都需要置位或者是复位。容易知道,$A[1]$每+2置位或复位,$A[2]$每+4置位或复位,$\cdots$,更一般地,$A[i]$每+$2^{i}$都要置位或者是复位,那么该bit就发生变化,所以第$i$位发生set或reset的次数总共为$n/2^i$

对这个通项$n/2^i$求和,值为$n$那么最多也就是($\log n+1$)个bit就可以表示
$$\sum_{i=0}^{\log n}\frac{n}{2^i}<n\sum_{i=0}^{\infty}\frac{1}{2^i}=2n$$

下面我们来记账:

操作Amortized CostActual CostAccounting Cost
Set211
Reset01-1

 


 

 

 4. Two Stacks, One Queue

用两个栈来实现队列!我们知道栈是先进后出,而队列是先进先出。我们可以用两个栈来实现队列。方法如下:
如果是Enque操作,就把元素push到栈S1中,如果是Deque操作,则分为两种情况

 

$$\left\{
\begin{array}{ll}
popall~S1, pushall~S2, pop~S2 & S2=\emptyset \\
pop~S2 & S2\neq \emptyset
\end{array}
\right.$$

 

我们假定基本的pop和push操作、以及判断是否栈为空的代价均为$O(1)$

所以,比较简单的Deque的Actual Cost为$1+1=2$,而比较复杂的Deque操作,则实际代价为$1+2t$,$t$为栈S1中的元素个数。
下面,我们进行平摊分析,这里的内在联系是,我们在push的时候,先存款2,用于将这个元素从S1中pop,再push到S2中去的代价,这样我们的两个Deque操作的代价就都是2了,只要S1中有元素,我们的存款就是非负的,记账如下

操作Amortized CostActual CostAccounting Cost
Enqueue312
Dequeue(normal) 220
Dequeue(complex)22+2t-2t

 


 

 5. Two Queues, One Stack

 

这是伪5,用两个队列来实现栈。情况略比上一种情况复杂些。实现方法:每次在进行pop操作的时候,要把队列中除最后一个元素外都转移到另一个队列中,然后再进行Deque来模拟pop。总是能保证一个队列为空,另一个队列非空。我们规定判断是否空队列的代价为$O(1)$,基本的Deque和Enque操作也都为$O(1)$

push操作:
如果两个队列都空,则Q1.Enque();
如果Q1非空但Q2空,则Q1.Enque();
如果Q1空但Q2非空,则Q2.Enque();

pop操作:
如果两个队列都空,则返回false;
如果Q1非空但Q2空,则循环执行Q2.Enque(Q1.Deque())一直到Q1中只剩下一个元素,此时返回Q1.Deque()的值,即为最后进入Q1的元素;执行之后Q1变为空,Q2非空(如果之前Q1中不止一个元素的话)。
如果Q1空但Q2非空,则循环执行Q1.Enque(Q2.Deque())一直到Q2中只剩下一个元素,此时返回Q2.Deque()的值,即为最后进入Q2的元素;执行之后Q2变为空,Q1非空(如果之前Q2中不止一个元素的话)。

算法分析:我们每次pop的时候最坏情况下都需要移动队列,每次的代价都是$n\times O(1)=O(n)$,所以我们的复杂度为$O(n)$;下面我们看看平摊分析的结果,假设非空队列中有$t$个元素:

push的Actual Cost为$2=1+1=$判空+Enque;

pop的Actual Cost为$2t=1+2(t-1)+1=$判空+转移+Deque;

每次的push的Accounting Cost都是$2n-2$,这样的话,我倒是觉得其实没有这么多啊,如果遇到pop操作的话,只需要存下$2t-2$这样的存款,这样pop操作的Amortized Cost就是0了,但是这样似乎失去了平摊分析的意义了啊。。。所以我觉得还是存下$2n-2$的存款吧,每个元素在pop的时候最多转移$n-1$次,所以最终push的Amortized Cost就是$n$,最后的复杂度为$O(n^2)$,显然不高效啊!!

伪5说的就是这个意思,是我大概随便写写的。也不知道这样分析对不对。。。


 

<下期预告: Adversary Argument >

转载于:https://www.cnblogs.com/godfray/p/4082648.html

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值