攤還分析(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)\)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章