攤還分析(1)——算法導論(23)


攤還分析(amortized analysis)是一種分析一個操作序列中所執行的所有操作的平均時間分析方法。與一般的平均分析方法不同的是,它不涉及概率的分析,可以保證最壞情況下每個操作的平均性能。

下面介紹癱瘓分析中的最常用的三種技術。

1. 聚合分析

1.1 棧操作

先來看對棧進行操作的例子。

通常,棧能夠進行push(S, x)pop(S)操作,其時間復雜度均為O(1)。現在定義一個新的操作multipop(S, k),它刪除棧S棧頂的k個元素(若不足k個,則全部彈出為止)。下面是操作multipop(S, k)的偽代碼:

while not stack-empty(S) and k > 0
	pop(s)
    k = k - 1

可以看出,multipop的時間代價為min(s, k),其中s是棧中元素的個數。

下面我們分析一個由n個push、pop和multipop組成的操作在一個空棧上的執行情況。

簡單地,根據上面對multipop操作時間代價(min(s, k))的分析,我們很快得出,一個multipop操作的最壞時間是O(n),因為棧的大小是n,因此n個操作的序列的最壞時間代價是\(O(n^2)\)。雖然這個結論是正確的,但是它卻不是一個確界。

從另一個角度考慮,pop操作的次數至多與push操作的次數相等,而push操作的次數至多為n次。因此一個由n個push、pop和multipop組成的操作的時間代價最多為O(n),分攤到每一個操作的時間代價就為O(n) / n = O(1)。

1.2 二進制計數器遞增

我們再來看二進制計數器遞增的例子。

二進制計數器是用一個數組來表示一個數,即數組每一個槽充當二進制數的一個數位,數組的第一個槽為最低位。

其遞增算法的思路是:首先將最低位記為當前位。判斷當前位是否為1,若是,則將該位置為0,當前位右移一位(進位操作);不斷重復以上過程直至當前位的值不為1或者當前位“溢出”。最后,如果沒有“溢出”,就將當前位的值置為1。

下面是python實現代碼:

def increment(num):
    i = 0
    while i < len(num) and num[i] == 1:
        num[i] = 0
        i += 1
    if i < len(num):
        num[i] = 1

假設這個數組長度位k。根據上面的代碼,我們可以發現,increment操作的最壞時間代價是 O(k),這在所有位上都是1時發生。因此,我們可以粗略地做出判斷:對初值位0的計數器執行n個increment操作的時間代價位O(nk)。

和前面一樣,事實上,這個上界是不確切的。因為increment操作不會在連續執行n次時,每次都是最壞情況。事實上,我們可以確切地計算出其時間代價:

\[ \sum_{i=0}^{k-1}⌊\frac{n}{2^i}⌋ < n\sum_{i=0}^{\infty } \frac{1}{2^i}< 2 n \]

因此,對於一個初值為0的計數器,執行一個n個increment操作的序列的最壞時間代價是O(n),分攤到每一個操作的時間代價為O(1)。

2. 核算法

在用核算法(accounting method)進行攤還分析時,我們對不同操作賦予不同的費用,它可能多於或少於其實際代價,我們稱其為攤還代價

當一個操作的攤還代價超出其實際代價時,我們將差額存入數據結構的特定對象,存入的差額稱為信用。對於后續操作中攤還代價小於其實際代價的情況,信用可以用來支付差額。

2.1 棧操作

回到棧操作的例子,其中各操作的實際代價為:

操作 實際代價
push 1
pop 1
multipop min(k, s)

我們為這些操作賦予如下的攤還代價:

操作 攤還代價
push 2
pop 0
multipop 0

在每次進行push操作時,我們花費2個單位的代價,1個單位用來支付其本身的實際代價,另1個單位可作為信用保存;當對棧中的任何一個元素進行pop(multipop也屬於pop)操作時,可用該元素在push時儲存的信用來支付差額。這樣就保證了在任何時刻的信用值是非負的

因此,總實際代價的上界為總攤還代價,即為O(n)。

這是因為\(\sum\limits_{i=1}^{n}c'_i \geq \sum\limits_{i=1}^{n}c_i\)在任何時刻都成立,其中\(c_i\)為第i個操作的真實代價,\(c_i'\)為其攤還代價。

2.2 二進制計數器遞增

同理,我們也可以用核算法來分析上述二進制計數器遞增的例子。

因為increment操作的時間效率與當前數組中為1的位數成正比,因此我們將翻轉的位數作為操作的代價。

對於一次置位操作(將該位值置為1的操作),我們設其攤還代價為2。在進行一次置位操作時,我們花1個單位的代價來支付其本身的實際代價,剩余1單位作為信用,用來支付將來可能的復位操作(將該位值置為0的操作)的代價。這樣就保證了在任何時刻的信用值是非負的。這樣就保證了在任何時刻的信用值是非負的

因此,總實際代價的上界為總攤還代價。而在一次increment操作中,只進行一次置位操作,因此總攤還代價O(n)。

3. 勢能法

核算法不同,勢能分析並不是將預付代價表示為數據結構中特定對象的信用,而是表示為“勢能”,簡稱“勢”。釋放勢能即可用來支付未來操作代價。我們將勢能與整個數據結構而不是特定對象相關聯。

假設我們對一個初始數據結構\(D_0\)執行n個操作。對每一個\(i=1, 2,...,n\),用\(c_i\)表示第i個操作的實際代價,\(D_i\)表示在數據結構\(D_{i-1}\)上執行第i個操作得到的結果數據結構。勢函數\(\Theta\)將每個數據結構\(D_i\)映射到一個實數\(\Theta(D_i)\),此值即為關聯到數據結構\(D_i\)的勢;並且定義第i個操作的攤還代價\(c_i'\)為:

\[c_i' = c_i + \Theta(D_i) - \Theta(D_{i-1}) \]

每個操作的攤還代價為其實際代價與其引起的勢能變化的和

於是,\(n\)個操作的總代價為:

\[\sum_{i = 1}^n c_i' = \sum_{i = 1}^n(c_i + \Theta(D_i) - \Theta(D_{i-1})) = \sum_{i=1}^nc_i + \Theta(D_n) - \Theta(D_0) \]

做一個移項,變形得:

\[\sum_{i = 1}^n c_i' - \sum_{i = 1}^n c_i = \Theta(D_n) - \Theta(D_0) \]

如果我們能定義一個勢函數\(\Theta\),使得\(\Theta(D_n) \geq \Theta(D_0)\),則總攤還代價\(\sum_\limits{i = 1}^n c_i'\)給出了總實際代價\(\sum_\limits{i = 1}^n c_i\)的一個上界。

由於我們不是總能知道要執行多少個操作,因此無法直接保證\(\Theta(D_n) \geq \Theta(D_0)\)。但是如果我們將勢函數的選擇條件變得嚴格,使得\(\Theta(D_i) \geq \Theta(D_0)\)對於所有的i都成立,那么就可以保證。

3.1 棧操作

我們再次回到棧操作的例子。這次我們采用勢能法來分析該問題。

對於勢函數的選取,我們選擇將棧映射到其內部元素個數的函數,即\(\Theta(D_i)表示第\)\(i\)次操作棧時,棧中元素的個數。對於初始的空棧,有\(\Theta(D_0) = 0\);並且由於棧中元素不可能為負,因此\(\Theta(D_i) \geq 0 = \Theta(D_0)\)

因此,以上\(\Theta\)函數定義的n個操作的總攤還代價為總實際代價的一個上界。

有了以上\(\Theta\)函數的定義,我們便可以計算棧上各種操作的攤還代價。

對於push操作,其實際代價為1,其引起的勢能變化也為1,因此其攤還代價為2。

對於pop操作,同理可得其攤還代價為0。

對於一次multipop操作,它會將\(k' = \min(k, s)\)個對象彈出,即其實際代價為\(k'\);引起的勢差變化為\(-k\),因此其攤還代價也為0。

綜上,每個操作的攤還代價都為O(1),n個操作的總攤還代價為\(O(n)\),總實際代價最壞為\(O(n)\)

3.2 二進制計數器遞增

同樣我們再用勢能法來分析二進制計數器遞增的例子。

與上面相似,我們將\(\Theta(D_i)\)表示為二進制計數器中,位上是1的位數。顯然\(\Theta(D_i) \geq \Theta(D_0) = 0\)。因此,總攤還代價為總實際代價的一個上界。

再分析總攤還代價。對於一次increment操作,其實際代價為\(k'\)次復位操作和1次置位操作,為\(k'+1\);其引起的勢差變化為\(-k'+1\),因此攤還代價為\((k'+1) + (-k' + 1) = 2\)\(n\)個操作的總攤還代價為\(O(n)\),因此總實際代價最壞為\(O(n)\)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM