前言:
對弈類游戲的智能算法, 網上資料頗多, 大同小異. 然而書上得來終覺淺, 絕知此事要躬行. 結合了自己的工程實踐, 簡單匯總整理下. 一方面是對當年的經典<<PC游戲編程(人機博弈)>>表達敬意, 另一方面, 也想對自己當年的游戲編程人生做下回顧.
承接上兩篇博文:
(1). 評估函數+博弈樹算法
(2). 學習算法
這篇博文回歸到博弈樹這邊, 具體闡述下博弈樹的優化手段, 為了游戲性添加的合理技巧.
啟發搜索:
博弈樹本質是極大極小的求解過程, 而alpha+beta剪枝則加速該求解過程.
讓我們來構建一個簡單的alpha+beta剪枝用例:
注: 紫色代表極大值求解, 綠色代表極小值求解.
通過人工演算和模擬, 整個博弈過程, 成功地減少了3個節點的計算量的, 效果一般.
這個過程, 我們是否有優化的余地呢? 讓我們調整下, 節點S1和S2的搜索順序.
與調整順序之前相比, 其alpha+beta剪枝的效果提升, 砍去了一個大分支, 減少了4個節點的計算量.
從這個例子中, 我們可以清晰的看到, 對於博弈樹而言, 其alpha+beta的剪枝效果, 和搜索順序是有一定關系的. 簡單的總結: alpha+beta效果, 對搜素的順序敏感.
於是我們找到了一個優化方向: 調整可行步的順序, 並優先搜索預期高的分支. 該技巧命名為: 啟發搜索. 常有人借助歷史值, killer步來構造啟發函數.
// 負極大值算法 int negamax(GameState S, int depth, int alpha, int beta) { // 游戲是否結束 || 探索的遞歸深度是否到邊界 if ( gameover(S) || depth == 0 ) { return evaluation(S); } // 依據預估(歷史, 經驗)對可行步, 進行排序 sort (candidate list); // 遍歷每一個候選步 foreach ( move in candidate list ) { S' = makemove(S); value = -negamax(S', depth - 1, -beta, -alpha); unmakemove(S') if ( value > alpha ) { // alpha + beta剪枝點 if ( value >= beta ) { return beta; } alpha = value; } } return alpha; }
此時的核心算法結構中: 添加了可行步排序過程(sort (candidate list)).
當然該過程是有一定代價的, 在alpha+beta剪枝效果提升和排序損耗需要均衡和折中. 一般采用計算簡單的預估函數即可.
讓我們回到黑白棋AI, 我們可以簡單選定, 預估函數等同於位置表, 即P(x, y) = Map(x, y). (Map 為 黑白棋棋面的位置重要度矩陣), 效果斐然.
置換表:
搞過ACM的人, 都知道DP求解的一種方式: 記憶化搜索. 本質就是把中間狀態保存, 減少重復搜索的一種技巧.
置換表的核心思想基本一致: 狀態保存, 減少重復搜索.
但置換表的難點不在於思想, 而在於狀態保存.
具體可以分析如下:
1). 游戲局面S本身占用空間大, 而且需要保存的狀態S集合多, 因此需要一個轉換函數F(S) => key, (key為不長二進制串, 或一個很大的整數)
2). 轉換后的key, 一一對應了某個具體局面S (沖突率很低可忽略, 或不存在)
讓我們以黑白棋來做個例子, 局面轉換為矩陣(0: 空白, 1: 黑棋, 2:白棋), 扁平化為字符串, 在借助強有力的Hash函數來轉化.
這邊展示了具體的流程, 其效果的好壞, 取決於Hash函數的選擇.
簡單采用MD5算法, 其實是可行的, 不過比較消耗CPU. Zobrist hashing算法也是備受推薦.
和記憶化搜索相比, 置換表對應的局面是, 只是中間的預測節點, 因此該狀態除了本身和游戲局面相關, 還和當前的搜索深度有關.
因此具體代碼可修正如下:
// 負極大值算法 int negamax(GameState S, int depth, int alpha, int beta) { // 判斷狀態已存在於置換表中, 且搜索深度小於等於已知的, 則直接返回 if ( exists(TranspositionTable[S]) && TranspositionTable[S].depth >= depth ) { return TranspositionTable[S].value } // 游戲是否結束 || 探索的遞歸深度是否到邊界 if ( gameover(S) || depth == 0 ) { return evaluation(S); } // 遍歷每一個候選步 foreach ( move in candidate list ) { S' = makemove(S); value = -negamax(S', depth - 1, -beta, -alpha); // 保存S'到置換表中, 當depth更深時. TranspositionTable[S'] <= (depth, value) If TranspositionTable[S'].depth < depth unmakemove(S') if ( value > alpha ) { // alpha + beta剪枝點 if ( value >= beta ) { return beta; } alpha = value; } } return alpha; }
總結:
啟發搜索和置換表, 兩者都是很好的思路, 前者通過調整搜索順序來加速剪枝效果. 后者通過空間換時間. 總而言之, 這些都是博弈樹上很常見的優化手段. 當然在具體游戲中, 需要權衡和評估. 下一篇講講出於游戲性的考慮, 如何進行優化和策略選擇.
寫在最后:
如果你覺得這篇文章對你有幫助, 請小小打賞下. 其實我想試試, 看看寫博客能否給自己帶來一點小小的收益. 無論多少, 都是對樓主一種由衷的肯定.