演算法之道:形而之上謂之道

發表於2011-10-26

1966年3月的一天,美國加州大學洛杉磯分校的Andrew J. Viterbi教授在給研究生講解纏繞編碼的時序譯碼演算法SDCD。但不管他如何講解,學生就是聽不明白。思來想去,Viterbi覺得學生不能理解的原因是該演算法的證明過於複雜。於是他開始考慮如何簡化這個證明。在經歷了持久的煩躁和困惑後,他靈感頓現:需要簡化的不是演算法的證明,而是演算法本身。於是Viterbi對SDCD演算法進行了少許修改,提出了基於Trellis的概率譯碼演算法。這個演算法就是後來著名的CDMA技術的基石。Viterbi也因此而身價暴漲(創立了高通公司,賺取了數十億美元)。

一種新演算法引來革命性的技術和財富的暴漲,演算法的作用不可謂不大。但理解演算法、改變人生,或者以演算法的思維來進行思考,卻對很多人來說是鏡中花、水中月,難以觸控。

從廣義上定義,演算法就是求解問題的步驟(指令)。由於計算機的程式是一條指令接一條指令(步驟)地進行,它實際上就是演算法的逐步展開。因此,演算法瀰漫在所有的軟體程式裡,堪稱計算機的靈魂。

理解靈魂當然不是件容易的事情。除了高度抽象外,演算法背後的邏輯也非常纏繞。在看過一個問題的演算法解答後,人們感到的不一定是輕鬆,反而可能是困惑。這些演算法是如何被發現或發明的呢?這些問題的解答者是如何想到特定的演算法呢?在某些情況下,人們還不一定問得出這樣的問題,因為對演算法本身可能還沒有看懂。其實,深刻掌握演算法的人並不多見。儘管很多人會在各種場合指點江山、激揚文字,儼然一副大師的架勢,但他們對演算法的理解可能十分膚淺。很多經常需要使用演算法的人,也不過停留在自發,而不是自覺的階段:對於見過的問題或者與見過的問題類似的問題知道如何作答,而對於那些與見過的問題沒有相似性或相似性很少或相似性不容易看出的新問題就一籌莫展了。

而各種演算法書籍中的內容堆積、枯燥陳述、邏輯凌亂甚至理解錯誤等現象則加劇了人們對演算法的畏懼。

市場上的演算法書籍琳琅滿目,但存在共同的問題:講述一大堆問題,羅列諸多演算法,但從根本上卻是就事論事;各種演算法設計或分析戰略之間沒有什麼邏輯遞進或層次關係,演算法各種戰略的順序在安排上非常隨意,與它們之間存在的因果關聯不相符合。比如,這些書籍在講動態規劃、靜態規劃、貪婪選擇、近似演算法等時,沒有考慮到不同戰略之間的邏輯遞進關係,只是隨意安排章節,用一些具體問題來講解這些戰略而已。結果就是沒有邏輯主線貫通,讀起來費力、分散,不能形成有機的整體。看這些書的唯一收穫是獲得各種具體問題的解答,但在見到一個新問題時,對到底應該使用何種設計戰略和分析戰略,或者應該以何種順序來嘗試各種戰略卻模糊不清甚至不得章法。此外,這些演算法書對演算法戰略並沒有進行提煉,而是凌亂地分散在各種問題的具體解答中,形不成一種高度,形散神更散,無法系統性地訓練讀者的演算法思維。

這些書作為工具書查閱倒也可以,但作為訓練演算法思維的讀物或者教材顯然力有不敵。

那麼如何培養演算法思維呢?答案就是演算法背後的邏輯。不同的演算法戰略看似不同,實則一脈相承,甚至從更高的層次上看就是同一種思維。所有的演算法戰略如分而治之、動態規劃、貪婪選擇、隨機化、近似演算法等只不過是同一思維的不同方面而已!它們之間存在邏輯和效率的遞進關係。明白了這一點,對演算法的把握就會達到一個新的境界。我們用眾所周知的最小生成樹問題為例來加以說明。

最小生成樹問題,圖中粗線組成一棵最小生成樹

圖1 最小生成樹問題,圖中粗線組成一棵最小生成樹

最小生成樹問題的定義如下。

給定輸入為:帶權重的連通無向圖,每條邊的權重為。

要求輸出:一棵連線所有節點的樹,並且為最小。

例如,圖1裡面的粗線條組成了該圖的一棵最小生成樹。

我們是如何獲得這棵最小生成樹的呢?或者最小生成樹問題該用什麼方法來解決呢?

最簡單的辦法當然是暴力戰略,即將所有的生成樹找出來,計算它們的權重,取出最小的樹即可。但此種戰略的成本高昂,其數量級為,這裡代表圖中的邊的條數,代表圖的節點數量。而這是一個難以令人興奮起來的階乘級。顯然,我們需要對演算法進行改進。那麼如何改進呢?

在面臨複雜問題時,人類通常選擇將問題簡化,即將複雜的大問題分解為簡單的小問題。在解決了小問題後,再將小問題的解合併為大問題的解。這就是所謂的“分而治之”。由於小問題比大問題更易解決,分治就成了上策,並演化為演算法設計的基礎戰略。對最小生成樹問題來說,就是將整個圖分解為兩(也可以是其他數量)個尺寸(節點數)相等或相近的子圖,分別在這兩個子圖上尋找最小生成樹,然後將尋找出的兩個最小生成樹合併起來即可。顯然,分解的成本為線性,但合併的成本是,這樣我們得到分治演算法的成本遞迴式為。按照大師解法,該遞迴式的解為。

這是最優解法嗎?仔細分析上述分治演算法的成本構成可以發現,在分解個子問題時會出現很多重複的下級子問題,重複解決這些相同的子問題顯然不是明智之舉。改進的辦法就是將重複的子問題解決一次,然後將結果存起來供以後使用,這就是動態規劃戰略。採用此種戰略後,最保守也可以將成本降低至(實際上,在略為優化後可將成本降低到)。

但這是最優解法嗎?其實還不是。仔細分析可以發現,由於構建的是最小生成樹,我們可以依次選擇圖裡面最小的邊作為最小成樹的邊,條件是新選擇的邊不與已經選擇的邊構成環路,直到有條邊入選為止。而這正是貪婪選擇戰略。由於將所有的邊排序的時間複雜性為,檢查一條邊被選中後是否與前面選擇的邊形成環路的時間複雜性為線性,因此整個演算法的時間成本為。如果,此演算法的效率將高於前面討論的標準分治戰略的效率。如果再使用改進的資料結構來支援貪婪選擇戰略,則該時間複雜性可以降低到(使用斐波拉契堆的攤銷時間)。

但貪婪選擇戰略還不是最優戰略。仔細分析上面最小生成樹的構造演算法,我們發現它的成本在於邊的選擇(排序是為選擇做的準備),而構造最小生成樹本身的成本只有。我們為什麼要花多於構造本身成本的成本來構造最小生成樹呢?假如我們知道哪些邊屬於一棵最小生成樹,則構造起來就不費力氣了。但要想知道哪些邊屬於最小生成樹,不是需要與其他邊進行比對嗎?也許我們並不需要。我們可以用一個隨機數來告訴我們一條邊是否屬於最小生成樹。準確地說,我們用拋硬幣來決定一條邊是否應該被納入到最小生成樹裡。這樣就可以將時間成本降低到線性。這就是隨機化戰略。

演算法戰略遞進中的最小生成樹構建成本

表1 演算法戰略遞進中的最小生成樹構建成本

這樣,隨著演算法戰略的遞進,尋找最小生成樹的成本不斷降低,且在隨機化戰略達到線性的最低點(表1)!

不過,細心的讀者可能會產生諸多問題。例如,貪婪選擇戰略下,為什麼會想到使用斐波拉契堆呢?斐波拉契堆的攤銷分析結果是如何得出的?在隨機化戰略下,如何保證所選的邊確實是最小生成樹的邊呢?另外,表1中的貪婪選擇戰略似乎與隨機化戰略不相上下:和不是一個數量級嗎(通常是大於的)?怎麼說隨著演算法戰略的遞進,成本不斷降低呢?

在貪婪選擇戰略下,我們每次選擇權重最小的邊加入到一棵初始為空的最小生成樹裡,被加入的邊不能與已加入的邊形成環路。在這種戰略下,演算法的最大成本在每次選擇最小的邊上。而堆是支援此種操作的最佳資料結構。但對普通的二叉堆來說,每次刪除堆頂(我們當然使用最小堆)元素時,需要對堆進行調整以保持堆的屬性,從而為後續的取最小邊操作打下基礎。由於此種調整的時間為對數級,使得整個最小生成樹的操作成本為。但如果我們改變思維,取消二叉堆要求每個節點度數不超過2的限制,則調整堆的操作成本將降低到常數級。在這種情況下,每次刪除堆頂元素後,直接將元素較大的子堆掛在元素較小的子堆下即可。這樣,最小生成樹的選邊操作的總成本似乎為。加上降距操作的成本,整個最小生成樹的成本就是。

等等,這似乎有一個問題:前面所述的堆調整操作為常數級的前提是刪除堆頂元素後出現的子堆個數為常數。否則,在一大堆的子堆中間選出最小的元素(從而將別的子堆掛在其下)則可能不是常數時間。因此,問題的關鍵在於確保刪除堆頂元素所產生的子堆個數為常數或者有限。而這種思路就導致了斐波拉契堆的出現。事實上,斐波拉契堆比這更進一步:合併操作也不是馬上進行,而是留下這些子堆,在子堆數超出一定的限制時才進行合併操作。此外,出現度數相同的堆(堆頂元素具有同樣子節點數)時也進行合併操作:將度數相同的堆合併成新的堆。由此,我們可以得出下面的斐波拉契堆的定義。

斐波拉契堆由一組普通的堆(非二叉堆)構成。

度為k的節點的任意一棵子樹的規模最大為(每個節點的子節點數不能超過)。

所有子堆的度數都不相同。

圖2給出的是一個斐波拉契堆。

斐波拉契堆結構示意圖

圖2 斐波拉契堆結構示意圖

在斐波拉契堆下,刪除一個堆頂所產生的子堆個數不會超過。因此,在留下來的子堆裡面尋找最小元素的時間成本也不會超過。但這樣似乎得不到我們想要的的時間。不過,仔細分析發現,在斐波拉契堆的限制下,不可能每個節點的子節點數都是。事實上,絕大部分節點的子節點數都很少。這樣少數幾個節點的高度數導致的高成本可以攤薄到大量的低度數節點的低操作成本上,從而將整個最小生成樹的操作成本降低到每次選邊操作為常數成本的境界。這就是攤銷分析的中心思想。

對於隨機化戰略的最小生成樹演算法,不會因為邊的選擇是隨機的,這條邊就一定屬於某棵最小生成樹。因此,我們在隨機挑選邊的時候也需要進行某種測試,以衡量此邊是否合適。而為了進行此種衡量,我們需要對邊進行某種劃分,但這種劃分和測試本身必須線上性級別上。如何做到這一點呢?鑑於篇幅限制,有興趣的讀者請參閱《演算法之道:從無有到無窮》。

對於數量級和的比較,在稠密圖的情況下,它們確實是一個數量級。但如果圖是稀疏的或者和是一個數量級,則歸結為,歸結為。如果節點數很多,將顯著高於。而且,貪婪選擇戰略的時間成本是在使用複雜的斐波拉契堆結構情況下,而且是攤銷時間!因此,從多方面考慮,隨機化戰略優於貪婪選擇戰略。

由本文可見,對最小生成樹問題的反覆推敲可以串起演算法裡面的全部設計戰略。當這些戰略被串起時,我們所看到的就不僅僅是獨立分散的個體戰略,而是一條平時所不見的演算法之道!雖然若隱若現,但對慧眼來說,它確確實實存在。這就是“形而之上謂之道”的演算法境界。

 

相關文章