zoukankan      html  css  js  c++  java
  • [算法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 Cost Actual Cost Accounting Cost
    push 2 1 1
    pop 0 1 -1
    multipop 0 $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{
    egin{array}{ll}
    1 & i eq 2^x,x in N \
    i+1 & i=2^x,x in N
    end{array}
    ight.$$

    直接对$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}<nsum_{i=0}^{infty}frac{1}{2^i}=2n$$

    下面我们来记账:

    操作 Amortized Cost Actual Cost Accounting Cost
    Set 2 1 1
    Reset 0 1 -1

     4. Two Stacks, One Queue

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

    $$left{
    egin{array}{ll}
    popall~S1, pushall~S2, pop~S2 & S2=emptyset \
    pop~S2 & S2 eq emptyset
    end{array}
    ight.$$

    我们假定基本的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 Cost Actual Cost Accounting Cost
    Enqueue 3 1 2
    Dequeue(normal)  2 2 0
    Dequeue(complex) 2 2+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 imes 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 >

  • 相关阅读:
    Xshell的一些使用方法和注意事项
    adobe premiere pro cc2015.0已停止工作 解决办法
    视频播放效果--video.js播放mp4文件
    centos 7.0 编译安装php 7.0.3
    centos 7.0 安装nginx 1.9.10
    centos 7.0 firewall 防火墙常用命令
    webstorm 更改默认服务器端口
    css3 动画效果 定义和绑定执行
    css 图片垂直居中总结
    JS 面向对象随笔
  • 原文地址:https://www.cnblogs.com/godfray/p/4082648.html
Copyright © 2011-2022 走看看