算法设计与分析笔记(2)

前言

该笔记以清华大学出版社《算法设计与分析:微课视频版》(2024年1月第1版)为教材,总结了该教材第3章知识点,该笔记可能有不准确或不严谨之处,所有以教材为主。

算法分析方法

概率分析

平均时间复杂度可以通过计算所有问题实例的运算时间,然后取平均值。但这种方式在面对有大量问题实例时会很乏力。这时候可以借助概率分析(Probabilistic Analysis)来帮助我们计算分析平均时间复杂度。

引例

给定具有n个数的数组A,回答是否存在一个指定的数x,如果存在,则给出其在数组的位置;否则回答不存在。设计代码如下:

Search (int x,int *A,int n)
{
    for(int i=0;i<n;i++)
    {
        if(*A==x)
        {
            return i;
        }
    }
    return -1;
}

由前篇笔记可知,其最好情形时间复杂度与最坏情形时间复杂度分别为Ω(1)与O(n)。若此时需要分析该算法的平均时间复杂度,便可利用概率分析来计算。

该数组共有n个位置,数字x出现在随机的其中一个位置的概率便是1/n。假设x出现在第k个位置上时,这种情形下需要比较k次,同时由于随机位置的概率为1/n,可得平均比较次数为,而整个算法的平均时间复杂度由平均比较次数决定,最后可知,其平均情形时间复杂度为Θ(n)。

总结

概率分析方法的核心思想是通过分析情形的概率分布来简化计算,但是在一般情况下,这种分析方法需要一个假设,即每个问题的实例都有同样的概率作为算法的输入而被算法计算(换句话说,会提前假设概率分布是均匀的,所有的实例概率是相同的)。有时面对实际情形,概率分布可能不会是均匀的,需要根据实际情形来调整我们的概率模型。

分摊分析

我们有时会遇到很复杂的算法,按照前面分析最坏情形时间复杂度地方法可能会高,而由于算法本身的复杂性使得用概率分析会很吃力,这时可以借助分摊分析(Amortized Analysis),来得出合理的时间复杂度。分摊分析通过研究执行一系列所需要的操作费用,来研究各个操作之间的关系。分摊分析能够证明:如果一系列操作的总费用是小的,则其中的一个操作费用也是小的,即使该算法中有某个操作的费用很高。

举个例子,父母每个月都会向孩子给一定量的生活费,孩子每个月的花销有多有少。如果父母每次已最高金额向孩子拨付,将会是一笔很大的开支。但是实际上可以利用分摊分析的思想,如果孩子上个月有剩余,那么这个月便可以少付一些生活费,而上个月剩余的钱也可以在下个月继续使用。

分摊分析有其限制,一系列数据结构的操作必须是相互关联的,从而才能利用分摊分析的思想。下面梳理分摊分析的三种方法。

合计方法

合计方法(Aggregate Method)分析由n个操作构成的序列在最坏的情况下的运行时间总和T(n)。因而在最坏情况下,每个操作的平均费用,或者说分摊费用可定义为T(n)/n(分摊费用的计算方法对每个操作都适用)。

引例

这里引用教材进栈与出栈的例子:给出三种对栈的操作Push(S, x)(进栈操作:将元素x压入栈S中)、Pop(S)(出栈操作:将栈S最顶端的元素弹出)、MultiPop(S, k)(特殊的出栈操作:弹出栈S顶端的k个元素,当k大于栈S中的元素时为清空栈S),操作如下图。

现假设有一空栈S,执行一个由Push(S, x)、Pop(S)、MultiPop(S, k)构成的序列,现对其所花费的时间进行分析。

(一)、简单最坏情况分析

假设每个 MultiPop(S,k) 操作都为最坏情形,则其最坏时间复杂度为O(n)(当k >= n 时弹出所有元素)。并假设这n次操作都是 MultiPop(S, k)。此时该序列的总时间复杂度为 O(n)*n = O(n^2 ),而平均每个操作的时间复杂度为 O(n^2)/n = O(n)。

这种分析有一种问题,它的假设前提是每个操作都是独立的,并没有考虑各个操作之间的关系与约束(Push(S, x) 对 Pop(S) 与 MultiPop(S, k) 的约束,只有进行了 Push 操作,才能进行 Pop 与 MultiPop),从而高估了实际成本。

(二)、分摊分析(合计方法)

在(一)的分析中,我们可以推出以下条件:总Push次数最多n次;总Pop(包括MultiPop)次数 = 总Push次数。

Push总费用 = O(1)*n = O(n);

Pop(包括MultiPop)总费用 = O(n);

序列总费用 = O(n) + O(n) = O(n);

平均每个操作的时间复杂度为 O(n)/n = O(1)。

通过分摊分析,得出了一个更加紧凑的上界,会更加贴近实际情况。

记账方法

记账方法(Accounting Method)的思想是对不同的操作赋予不同的费用,对于某操作赋予的费用称为分摊费用。分摊费用可能比实际的多,也可能比实际的少。当分摊费用比实际费用多时,多出来的费用(即余款)将作为存款保存在数据结构中的一些特定的对象中。这笔存款将支付给分摊费用比实际费用少的操作中。一个操作的分摊费用便可以分为两个部分:1、实际费用;2、存款或透支。

如果要利用分摊费用来证明在最坏情况下每个操作的费用是小的,那么操作序列的总分摊费用必须是其总实际费用的上界,从而保证与该数据结构的总存款始终为非负值。记分摊费用为C1,实际费用为C2,则须满足

引例

再次引用之前栈操作的例子来描述记账方法。

表1    各个栈操作的实际费用
栈操作实际费用
Push1
Pop1
MultiPopmin{s, k}
表2    各个栈操作的分摊费用
栈操作分摊费用
Push2
Pop0
MultiPop0

 

当Push一个元素时,这个元素的分摊费用为2,由于实际费用为1,所以每个被Push的元素都有1存款。当Pop一个元素时,这个元素便透支了1费用,由于每个元素都有1费用的存款,于是不必给这个元素分摊费用。MultiPop元素同理,不需要分摊费用。所以不论进行何种操作,总实际费用永远不会超过总分摊费用,此时总分摊费用便是总实际费用的上界,故总实际费用不会超过O(n)。

势能方法

一根弹簧有弹性势能,当对这根弹簧进行操作(压缩、拉伸)时,弹簧的弹性势能会随着操作的变化而变化。类似于这种现象,势能方法(Potential Method)因此得名。这种方法先将数据结构的当前状态映射到一个非负实数,表示系统的“能量”;而分摊成本 = 实际成本 + ,其中(本次操作时的势能 - 上一次操作时的势能)。

引例

同样以栈操作来举例:

定义势能函数: = 栈中当前的元素量

各种栈操作实际费用参考表1,每个操作的分摊成本 = 实际成本 + 

Push操作的分摊成本(PuC) = 1 + (+1)= 2;

Pop操作的分摊成本 (PoC)= 1 + (-1) = 0;

Multi Pop操作的分摊成本 (MPoC)= k + (-k) = 0;

总分摊成本 = PuC + PoC + MPoc = 2 + 0 + 0 = 2;

由于有n次操作,所以总分摊成本 = 2n;最终可得时间复杂度为O(n)。

总结

概率分析属于两者中较为简单的一种,在面对概率分布较为均匀,或者说可以总结出概率分布模型时可以考虑用概率分析来简化计算;如果遇见算法本身比较复杂时可以考虑使用分摊分析,但在使用分摊分析时要注意各个操作之间的关联性,不能过于独立各个操作;如果操作较为单一,可以使用合计算法,核心思想是计算整个操作序列的总成本,再均摊到每个操作上(单次分摊成本=总成本/操作次数);在操作类型简单、信用分配直观的问题上时,可以使用记账方法,主要为每个操作分配一个虚拟的“分摊费用”,用存款补齐透支,但不能“欠债”;在面对操作类型复杂,需全局分析的问题时,可以使用势能方法,核心是“能量守恒”思想,分摊成本 = 实际成本 + 在数学上势能方法与记账方法等价。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值