從這一篇文章開始,筆者開始了對《算法的樂趣》一書的學習。與以往筆者看的面向競賽的算法數和經典教材不同,這本書接介紹的算法多為在現實生活中或者已經應用在生產實踐當中的算法,比如說這篇文章所介紹的博弈樹,就是前段時間非常火的人與AI的圍棋大戰的基礎。
需要提前說明的一件事情是,由於本書當中的算法有非常好的應用與實踐性,但是受筆者能力和經歷所限,可能無法非常給出並分析算法的源代碼,因此筆者在文章中介紹這些算法的時候,也主要以算法思想和偽代碼為主,如果讀者對某個算法的源代碼感興趣,可以留言筆者會將原作者所寫的源碼發送給你。
那么我們開始正文。
首先,我們來拋出一個問題,既IBM的深藍打敗國際象棋大師卡斯帕羅之后,也就在最近,Google的阿爾法狗又攻陷了人類智力的最后堡壘——圍棋。那么我們一定會好奇,沒有生命的計算機何以強大到如此地步能夠在這些以智力稱道的游戲中打敗人類呢?那么接下來我們就要一一揭開棋類AI的神秘面紗。
博弈:
首先我們應該對我們要探討的問題有充分的了解,不管是圍棋還是國際象棋,他們本質上都能叫做“博弈”,那么什么是博弈呢?博弈可以理解為有限參與者進行有限策略選擇的競爭性活動,比如下棋、打牌、競技、戰爭等。而在這里我們將博弈更進一步簡化,我們探討一些簡單的“二人零和、全信息、非偶然”的博弈。所謂零和,就是有輸必有贏,最多出現平局,不會出現雙贏的博弈;而“全信息”則指博弈過程雙方對戰局信息的了解是公開和透明的,即一種信息對稱;而所謂“非偶然”,即規定博弈的參與者都是理性而聰明的。
初步了解了博弈的定義,我們從一個最簡單的博弈開始探討。
以井字棋游戲為例的極大極小值搜索算法:
首先來介紹井字棋游戲,非常類似五子棋,在一個3x3的方格上,雙方輪流填充“x”和“o”,先使得3個“x”或“o”相連的一方獲勝。
我們設玩家Max填充“x”,玩家Min填充“o”,Max先手。那么這里我們應該意識到的一個很重要的思想:計算機能夠做出的一切看似“聰明”的策略,其實都是都是在較短時間內模擬出了所有棋局狀態然后篩選出對AI最有利的狀態。
我們來看這樣一個圖。

在這個圖中,Player1其實就是上文中我們定義的Max,容易看到,這里是列出了前兩步的所有狀態的樹結構,這種記錄棋局狀態的樹狀結構變叫做博弈樹。
那么容易看到,我們可以順着這個博弈樹,得到所有的棋局狀態,那么現在的問題是,假設我們已經得到了一個完全的博弈樹,我們如何優化AI的決策呢?
這便是我們要介紹的極大極小值搜索算法,我們從Max即Player2的角度,設置一個權值w給博弈樹中的每個節點,用來表征這個節點(某一種狀態)對Max的有利程度,這個權值越大,表示這種狀態對Max更有利。
那么我們在模擬對弈的過程中,對於第一層樹也就是Max填充“x”,由上面的定義,顯然Max傾向選擇節點權值較大的節點,即w[1] = max(w[2],w[2],w[3]),因此我們需要找到博弈樹第二層三個節點的權值,這便需要繼續拓展博弈樹的第三層。
而對於玩家Min來說,它顯然傾向於選擇對自己更有利的棋局狀態,即w最小的節點,由此我們可知w[2] = min(w[5],w[6],w[7],w[8],w[9]),同時w[3]、w[4]也是類似的方法求解。
是否找到了規律化的模式呢?容易看到,這是一個遞歸算法,對於第i層落子的策略,不論對於Max還是Min,它都要基於第i+1層的所有狀態,只不過不同的是,對於玩家Max,他要選擇該節點所有子節點的權值最大值,而Min則要選擇最小值。
最終,我們需要將博弈樹拓展到葉節點,然后回溯回去得到應對各種情況的最有利策略。

基於對這種思想理解,我們不難寫出下面極大極小值算法的偽代碼。
int MiniMax(node,depth,isMaxPlayer) { if(depth == 0) return Evaluate(node); int score = isMaxPlayer ? -INF : INF; for_each(node的子節點child_node) { int value = MiniMax(child_node,depth-1,!isMaxPlayer); if(isMaxPlayer) score=max(score,value); else score=min(score,value); } }
其實我們能夠看到,這種博弈樹是基於搜索或者窮舉或者dp的思想都可以,這不難理解,這是很基本的編程思維,雖然可以說這是設計棋類AI很核心的部分,但是這樣就能打敗人類了么?當然不能,這種算法設計模式面臨的一個最大的問題是,博弈樹的規模太過龐大,當時計算能力超強的計算機也無從招架,因此在真正的算法設計中,我們需要需要限制搜索深度和各種各樣的剪枝如阿爾法貝塔剪枝、A*剪枝等算法來削減博弈樹的規模,這便是我們下面要介紹的內容。
