本周的內容是Amortized Analysis,是對算法復雜度的另一種分析。它的基本概念是,給定一連串操作,大部分的操作是非常廉價的,有極少的操作可能非常昂貴,因此一個標准的最壞分析可能過於消極了。因此,其基本理念在於,當昂貴的操作特別少的時候,他們的成本可能會均攤到所有的操作上。如果人工均攤的花銷仍然便宜的話,對於整個序列的操作我們將有一個更加嚴格的約束。本質上,均攤分析就是在最壞的場景下,對於一連串操作給出一個更加嚴格約束的一種策略。
均攤分析與平均情況分析的區別在於,平均情況分析是平均所有的輸入,比如,INSERTION SORT算法對於所有可能的輸入在平均情況下表現性能不錯就算它在某些輸入下表現性能是非常差的。而均攤分析是平均操作,比如,TABLEINSERTION算法在所有的操作上平均表現性能很好盡管一些操作非常耗時。在均攤分析中,不涉及概率,並且保證在最壞情況下每一個操作的平均性能。
有三類比較常見的均攤分析:
1.聚類分析:證明對所有的n,由n個操作所構成的序列的總時間在最壞情況下為T(n),每一個操作的平均成本為T(n)/n;比如棧的操作,對於一個空棧的入棧和出棧的操作
2.記賬方法:在平攤分析的記帳方法中,決定每一個操作的均攤成本,對不同的操作賦予不同的費用,某些操作的費用比它們的實際代價或多或少。我們對一個操作的收費的數量稱為平攤代價。當一個操作的平攤代價超過了它的實際代價時,兩者的差值就被當作存款(credit),並賦予數據結構中的一些特定對象,可以用來補償那些平攤代價低於其實際代價的操作。這種方法與聚集分析不同的是,對后者,所有操作都具有相同的平攤代價。數據結構中存儲的總存款等於總的平攤代價和總的實際代價之差。注意:總存款不能是負的。在開始階段對於過度要價存儲預先支付的存款,在后面的序列中再支付操作。比如,二進制計數器: 通過二進制觸發器計算一系列數字
3.勢能方法:在平攤分析中,勢能方法(potential method)不是將已預付的工作作為存在數據結構特定對象中存款來表示,而是將存款總體上表示成一種“勢能”或“勢”,它在需要時可以釋放出來,以支付后面的操作。勢是與整個數據結構而不是其中的個別對象發生聯系的。比如,動態表,可以動態改變大小的連續存儲數組。
一、聚類分析
在聚類分析中,對於一連串的n的操作,我們計算總的最壞時間T(n). 在最壞情況下,每一個操作的平均成本或者均攤成本是T(n)/n. 成本T(n)/n適用於每一個操作(可能有幾種類型的操作)。另外兩種方法可能將不同的均攤成本分配給不同類型的操作。
比如,有MULTIPOP操作的棧。有兩種基本的棧操作都分別花費O(1)的時間: PUSH(S,x)和POP(S)分別是將對象x壓入棧中,從棧S的頂部彈出並返回彈出的對象。將每一個操作的花銷都賦為1. 一連串n個PUSH和POP操作的總消耗為n,對於n個操作的實際運行時間為O(n).
現在添加一個額外的棧操作MULTIPOP。MULTIPOP(S,k) 是彈出棧S的前k個對象(或者彈出整個棧如果k大於棧的大小的話)。
MULTIPOP的總消耗是min{|S|,k}.
現在考慮在一個初始為空的棧上的一序列n個POP,PUSH和MULTIPOP操作。算法偽代碼如下:
下面為一個例子:
粗略地分析,MULTIPOP (S,k)將會花費O(n)的時間,因此,
在操作序列中,一些操作可能會很廉價,但是一些操作可能會非常昂貴耗時,比如MULTIPOP(S,k). 然而,最壞的操作往往不是經常被調用的。因此,傳統的最壞的單一操作分析會給出過於消極的邊界。
我們的目標是,對於每一個操作,我們希望能夠賦予其一個均攤的成本來對實際的總的成本進行定界。對於n個操作的任意序列,我們有
這里,是表示第i步的實際成本。
使用聚類分析使得有更加緊湊的邊界分析,對於所有的操作都有相同的均攤成本.
觀察得知,POP操作的數目一定小於或者等於PUSH操作的數目。因此,我們可以得到:
因此,平均來看,MULTIPOP(S,k)這一步將花費O(1)而不是O(k)的時間。
這里來看另一個例子,考慮一個從0開始計數的k位的二進制計數器。使用位的數組A[0,…, k-1]來記錄計數。存儲在計數器中的二進制數在A[0]有最低階的位,在A[k-1]有最高階的位,並且有
初始時,x=0, 對於i = 0,… k-1, 都有A[i]=0
一個存儲案例如下:
INCREMENT算法是用來在計數器中加1(2^k)到一個值上。
算法偽代碼描述為:
考慮從0開始計數的n個操作的一個序列:
那么粗略計算,我們可以得到T(n)<= kn,因為一個增加操作可能會改變所有的k位。
我們使用聚類計數來緊湊分析的話,有基本的操作flip(1->0)和flip(0->1)
在n個INCREMENT操作的一個序列中,
A[0] 每一次INCREMENT被調用的時候都會flip,因此flip n次;
A[1] 每兩次調用INCREMENT時flip,因此flip n/2次;(通過列中標記的黃色可以看出來規律)
…
A[i] flips 次.
因此,
每一個操作的均攤成本為: O(n)/n =O(1).
二、記賬方法
記賬方法的基本思路為,對於每一個有實際成本COP的操作OP而言,均攤成本被分配使得對於n個操作的任意序列,有
如果,那么額外的部分就可以被存儲為預付的存款(credit),這筆存款可以在之后對於
的操作時被用。
這樣的要求實質上是使得存款不會為負。
我們回到有MULTIPOP操作的棧的問題,對於這樣的棧,將均攤成本分配為:
其中,credit是棧中條目的數目。
從一個空棧開始,n1個PUSH,n2個POP和n3個MULTIPOP操作的任意序列最多的花銷是 ,這里,n = n1 + n2 + n3.
需要注意的是,當有超過一種類型的操作時,每一種類型的操作可能被賦予不同的均攤成本。
下面通過一個銀行家的觀點來看記賬方法。假如你正在租一個操作硬幣的機器,並且根據操作的數量來收費。那么有兩種支付方法:
A. 對每一種實際的操作支付實際費用:比如PUSH支付1元,POP支付1元,MULTIPOP支付k元
B. 開一個賬戶,對每一個操作支付平均費用:比如PUSH支付2元,POP支付0元,MULTIPOP支付0元
如果平均花銷大於實際的費用,那么額外的將被存儲為credit(存款);如果平均成本小於實際的花費,那么credit將被用來支付實際的花費。這里的限制條件為:
對任意的n個操作, ,也就是說,要保證在你的賬戶中有足夠的存款。
下面是一個例子:
對於之前的二進制計數器有一樣的道理,賦予均攤成本為:
我們可以觀察到flip(0->1)的數目大於等於flip(1->0),因此有
三、勢能方法
勢能方法是從一個物理學家的角度出發看問題,基本思路是有勢,對於每一個操作OP直接設置不是那么簡單。因此,我們定義一個勢能函數作為橋梁,也就是,我們將一個值賦給一個狀態而不是賦給一個操作,這樣,均攤成本就是基於勢能函數來計算的。
定義勢能函數為: 其中S是狀態集合。
均攤成本的設置為: ,因此我們有
為了保證 ,足以確保
對於棧的例子,令表示棧中的條目的數目。實際上,我們可以簡單講存款作為勢能。這里狀態Si表示在第i個操作之后棧的狀態。對於任意的i,有
。
因此,棧S的狀態為:
那么勢能函數 的折線圖表示為下圖:
我們如下定義:
因此,從一個空棧開始,n1個PUSH,n2個POP和n3個MULTIPOP操作的任意序列花費最多
,這里n = n1 + n2 + n3.
在二進制計數器中,在計數器中將設置為勢能函數:
此時,勢能函數 的折線圖表示為:
在計數器中將設置為勢能函數,在第i步,flips Ci的數目為:
因此,我們有
換句話說,從00…0開始,n個INCREMENT操作的一個序列最多花費2n時間。
下面考慮一個實際的問題:
假設現在我們被要求開發一個C++的編譯器。Vector是一個C++的類模板來存儲一系列的對象。它支持一下操作:
a.push_back: 添加一個新的對象到末尾
b.pop-back:將最后一個對象彈出
注意vector使用一個連續的內存區域來存儲對象。那么我們該如何為vector設計一個有效的內存分配策略呢?
這就引出了動態表的問題。
在許多應用中,我們不能夠提前知道在一個表中要存儲多少個對象。因此,我們不得不對一個表分配一定空間,但最后發現其實不夠用。下面引出兩個概念:
動態擴展:當在一個全表中插入一個新的項時,這個表必須被重新成一個更大的表,原來表中的對象必須被拷貝到新表中。
動態收縮:相似的,如果從一個表中刪除了許多的對象,那么這個表可以被重新分配成一個尺寸變小的新表。
我們將給出一個內存分配策略使得插入和刪除的均攤成本是O(1).,就算一個操作觸發擴展或者收縮時其實際成本是較大的。
動態表擴展的例子:
考慮從一個空棧開始的操作的一個序列:
Overflow之后擴展表的操作:
粗略地分析,考慮這樣的一個操作序列,如果我們根據基本的插入和刪除操作來定義成本,那么第i個操作的實際成本Ci是
這里的Ci = i是當表為滿的時候,因為此時我們需要插入一次,並且拷貝i-1項到新表中。
如果n個操作被執行了,那么一個操作的最壞情況下的成本將為O(n). 這樣的話,對於總的n個操作的總運行時間為O(n^2),並不如我們需要的緊湊。
對於以上情況,我們如果使用聚類分析:
首先觀察到表的擴展是非常少的, 因為在n個操作中表擴展不常發生,因此O(n^2)的邊界並不緊湊。
特別的,表擴展發生在第i次操作,其中i-1恰好是2的冪。
因此,我們可以將Ci分解為:
這樣n個操作的總花費為:
因此,每一個操作的均攤成本為3,換句話說,每一個TABLEINSERT操作的平均成本為O(n)/n=O(1)
如果我們使用記賬方法:
對於第i次操作,一個均攤成本被支出。這個費用被消耗到運行后面的操作。任何不是立即被消耗掉的數量將被存在一個“銀行”用於之后的操作。
因此,對於第i個操作,$3被用在以下場合:
A.$1支付自身插入操作
B.$2存儲為之后的表擴展,包括$1給拷貝最近的i/2項和$1給拷貝之前的i/2項
如圖:
存款絕不會為負。換句話說,均攤成本的和給出了實際成本的和的一個上界。
如果我們使用勢能方法:
銀行賬戶可以被看做一個動態集合的勢能函數。更加明確來說,我們希望有一個這樣性質的勢能函數:
a.在一次擴展之后,
b.在一次擴展之前, ,因此,下一次擴展可以通過勢能支付。
一個可能的情況:
其折線圖為:
初始時, 並且非常容易驗證當表總是至少半滿的時候有
。那么關於
的成本
被定義為:
這樣的話, 就是實際操作的一個上界了。
下面分的兩種情況來計算
:
Case-1:第i次插入不會觸發一個擴展
此時, , 這里,numi表示第i次操作之后表項的數目,sizei表示表的大小,Ti表示勢能。
Case-2:第i次操作觸發了一個表的擴展
此時,
因此,從一個空表開始,一個n個TABLEINSERT操作的序列在最壞情況下花費O(n).
刪除操作是類似的分析。
總的來說,因為每一個操作的均攤分析是被一個常數給界頂了,因此如果是從空表開始,在一個動態表上的任何n個TABLEINSERT和TABLEDELETE操作的序列的實際花銷都是O(n).
均攤分析可以為數據結構性能提供一個清晰的抽象。當一個均攤分析被調用時,任何的分析方法都可以被使用,但是每一種方法都有一些是被有爭議為最簡單的情況。不同的方法可能適用於不同的均攤成本賦值,並且有時可能得到完全不同的界。