平攤分析 --- 算法導論讀書筆記

       我們經常會說一個算法快不快,這個可以由實驗得出,也可以通過分析複雜度得出。實驗需要大量不同的輸入才更全面準確,否則片面地看某個輸入下的表現,是比較偏頗的。分析複雜度(通常分析最壞,因爲平均涉及輸入的概率分佈,依靠假設或者實驗和經驗)有時候並不是一個簡單的事,簡單的情況是遍歷 for(int i = 0; i != n; i++) 的這種情況,顯然是O(n)的複雜度。但是一些複雜的情況就比較難辦了,舉例來說:

         a.   棧操作:  除了PUSH,POP,添加一個操作叫MULTIPOP。

                     
MULTIPOP(S,k)
   while not EMPTY(S) && k != 0
       do POP(S)
           k <- k-1

                 假設棧的大小爲s,那麼MULTIPOP的複雜度爲min(s,k)。
                 那麼現在有一個空棧,進行n次操作,三種操作各種可重排列都有可能。問這n次操作的複雜度是多少?

         b.    二進制計數器: 一個k位的二進制數A,不斷加1。這個二進制數用k位數組表示,每一位爲0或者1。 加1的操作叫INCREMENT。這個操作leetcode上也正好有類似的題目Plus One,它是十進制數。
             
INCREMENT(A)
  i <- 0
  while i < length[A] && A[i] == 1
      do A[i] <- 0
          i <- i+1
  if i < length[A]
     then A[i] <- 1
                顯然最差情況末尾k-1個1的時候,複雜度爲O(k)。
                那麼,現在從0開始加到n。問複雜度是多少?

      初看上去,第一個問題MULTIPOP最差是當有n-1個元素時,之前PUSH了n-1次,需要O(n),也就是一次操作最差O(n),n次操作最差O(n*n)。第二個類似地也是O(n*n)。但是這個複雜度估計得太寬鬆了,要求更精確一點的複雜度就不是一眼看出的了。這個時候,引入了平攤分析。

      平攤分析被引入實際上是爲了解決某操作被多次調用,不同情況下各次調用複雜度不同的問題。該操作如果分別考慮不同情況的各次來計算,過於複雜,而作爲一個整體的時候,考慮該操作各情況調用的平攤複雜度,計算起來會容易不少(或者說更容易描述)。用書上的話說,“平攤分析可以用來證明在一系列的操作中,通過對所有操作求平均之後,即使對其中單一的操作具有較大的代價,平均代價還是很小的。平攤分析與平均情況分析的不同情況在於它不牽涉到概率;平攤分析保證在最壞的情況下,每個操作具有平均性能。”後面一句就是表示,單次操作平攤代價累積起來,大於等於總的最壞情況,當然,從複雜度上儘可能地逼近最壞情況。

       介紹了三種分析方法:1.聚集分析;2,記賬方法;3勢能方法。

       1.聚集分析
       所謂聚集分析,實際上就是從整體考慮,考慮多次操作總的複雜度。
       比如問題a,對於一個空棧,n次操作的複雜度爲O(n)。因爲對象被壓入後最多被彈出一次,則調用POP的次數(包括了MULTIPOP裏的POP)最多等於PUSH的次數,最多爲n。通常我們算總的複雜度其實就到此爲止了,一定要算出平攤複雜度的話就除以n,三個棧操作的平攤複雜度都是O(1)。
       問題b中,對A的操作,每次調用,A[0]都會翻轉,A[1]每兩次翻轉一次,A[2]每4次翻轉一次...如果進行n次操作,那麼他們分別有n,n/2,n/4...(下取整)次翻轉。等比數列,加起來爲小於2n次。n次總複雜度爲O(n),單次的平攤代價爲O(1)。

       2.記賬方法
       書上原話是“我們對不同的操作賦予不同的費用,某些操作的費用比它們的實際代價或多或少。我們對一個操作的收費的數量稱爲平攤代價。”。其實,操作起來,就相當於我先假設單次某操作的代價,假如我估計某費時的瓶頸操作平攤複雜度爲O(1),那我完全可以讓單次操作的代價爲某一個常數k(通常要略大於這個操作的實際代價),多的存下來,可以把存款放出去給其他操作。那麼,因爲有的操作會存,有的操作會花,不同操作有不同的平攤代價,聚集分析中平攤代價是一樣的。要保持分析中代價一直是上界,存款不允許爲負
       對問題a,令PUSH代價爲2。相當於1用來支付PUSH,同時放一個在上面,POP的時候拿走。那麼POP和MULTIPOP都爲O(0)。總的爲O(n)。
       對問題b,把某一位設爲1代價爲2,存1用來支付把這一位設爲0,那麼設爲0代價爲0。總的爲O(n)。

      3.勢能方法
      勢能方法英文叫potential method,勢能還是翻譯得很好的,因爲它是與數據結構相關而不是與動作相關。比如拿棧來說,棧裏有多少元素我們就認爲它有多少勢能,這是一種可能的做法,這也正是這個結構的potential。平攤代價就由實際代價加上增加勢能變化量。CP(i) = C(i) + S(i) - S(i-1)。n個操作就是ΣCP(i) = ΣC(i) + S(n) - S(0)。要保持上界,就得讓S(n)-S(0)永遠非負。通常,讓S(0)等於0,那麼接下來S(n)>= 0就好。我覺得勢也是一種存款。因而平攤操作重點在於考慮勢的增加而不是減少(勢差爲負可以用來減實際代價,使平攤代價變少,如下ab問題)
       對問題a,把勢函數定義爲棧裏元素個數。那麼PUSH平攤就爲1+1=2,POP1-1= 0,MULTIPOP同POP一樣也爲0.所以O(n)。
       對問題b,把勢函數定義爲計數器中1的個數。第i次操作復位t(i),置位1,C(i)=t(i)+1,勢差爲1-t(i),所以平攤代價爲2.所以爲O(n)。
       其實,注意如果考慮S(n)-S(0)的複雜度,那麼就可以不要求S(n)-S(0)。因爲ΣCP(i) = ΣC(i) + S(n) - S(0),現在知道了ΣCP(i)的複雜度,又知道 S(n) - S(0),顯然可以知道ΣC(i) 的複雜度。

       最後,書裏用動態表的操作來爲一個例子再闡述了分析過程。這裏我用KMP爲例來闡述。論KMP-COMPUTE-PREFIX過程。
       
KMP-COMPUTE-PREFIX(P)

m <- length[P]
π[1] <- 0
k <- 0               // just like q above, number of char matched
for q <-  1 to n     // like i above, and actually it is state, so its name is q
  while k>0 && P[k+1] != S[q]  // P range from 1 to n, not 0 to n-1
    do k <- π[k]
  if P[k+1] == S[q] then k <- k+1
  π[q] <- k
return π

      首先,用記賬法。對π[q]賦值這個操作我們給它記2,1用於自己的開銷,存1到q位置上。if P[k+1] == S[q] then k <- k+1 這個開銷複雜度是等同於π[q] <- k,自然可以用π[q]賦值這個操作代表了。問題在於while循環,這個循環是一個讓k退步的一個操作,它不斷地取π[q],這個取的開銷可以用放在上面的存款支付了.由於π[q]總是指向一個有存款的地方(一開始我們在0,1兩處各放上1,這是O(1)的)。所以總是有存款可花的,保證了上界。也就是while循環平攤爲了0.總開銷爲O(n)。
     也可以用勢能法。也是書上的方法(在KMP一節)。k的勢就是當前狀態k。顯然開始爲0,以後總大於等於0.顯然勢最多加1,因爲狀態最多前進一步(也就是if語句)。後退(也就是while循環)是可以很多的,這些代價被負的勢差所抵消。while和if都是在操作勢,後面賦值的開銷是O(1),勢的平攤開銷(至多)是O(1).所以最後爲O(n)。
      對KMP-MATCH(S,P) 可以用類似的方法,不再贅述。



轉載註明出處

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