博弈論 | 詳解搞定組合博弈問題的SG函數
本文始發於個人公眾號:TechFlow,原創不易,求個關注
今天這篇是演算法與數據結構專題的第27篇文章,我們繼續深入博弈論問題。今天我們要介紹博弈論當中非常重要的一個定理和函數,通過它我們可以解決許多看起來雜亂無章的博弈問題,使得我們可以輕鬆地解決一大類博弈問題。
有了SG函數和SG定理,我們不再是單純地通過構思、分析和找規律去解決問題了。並且我們之前學過的巴什博奕、威佐夫博弈以及Nim博弈都可以使用SG函數來解決,相當於我們找到了這一大類問題的通解。下面,我們來看幾個基本定理和基本概念。
基本定理
ICG遊戲
前面我們說了,SG函數和SG定理可以解決一大類的博弈問題。這一大類的博弈問題稱為ICG遊戲,我們之前介紹過的三種博弈模型,本質上都屬於ICG遊戲。
關於ICG遊戲,它的定義如下,需要滿足三個條件:
-
遊戲有兩人參與,兩人輪流做出決策,並且兩人做出的決策都是對自己最優的 -
當有一人無法決策的時候,該人失敗。無論兩人如何決策,該遊戲都必然會在有限時間內結束 -
遊戲中同一個狀態不能達到多次,且遊戲沒有平局。遊戲者在某個確定狀態做出的決策集合只與狀態有關,與遊戲者無關
必勝態與必敗態
也就是奇異狀態與非奇異狀態,我們定義P狀態是必敗態,N狀態是必勝態。我們可以簡單理解成,在P狀態的玩家一定會輸,而在N狀態的玩家一定會贏。
這一點在之前的Nim取子的文章當中我們曾經深入地分析過,展開來說,其實也有三條:
-
無法移動的狀態為P狀態 -
可以移動到P狀態的狀態為N -
所有移動都會進入N局面的局面為P
我們曾經在分析威佐夫博弈問題的時候,將遊戲局面抽象成了二維平面坐標系當中的點。其實所有ICG遊戲都可以想像成一張有向無環圖(DAG),遊戲開始時有一顆放在起點的棋子,兩個玩家輪流移動棋子,直到不能移動的玩家落敗。所有隻能移動到終點的局面都是必勝的,所有隻能連接必勝點的點是必敗的。我們用遞歸的思路可以計算出所有點的狀態。
當然我們用演算法去搜索遍歷所有狀態這耗時太多了,我們可以通過一個函數來計算它,這就是我們今天文章討論的重點——SG函數。
Sprague-Grundy數的推導
SG是Sprague-Grundy的縮寫,我沒有記錯,這應該是兩個人名,它使用起來非常簡單,但是推導過程有些複雜。如果我們忽略推導過程直接去研究它的使用的話,你會有一種在運用魔法的感覺。因為你完全猜測不到它其中的原理,所以我們需要詳細解釋一下它的推導過程,這樣才能加深理解。
我們先明確幾個概念,首先對於ICG遊戲來說,失敗的最終狀態只有一個,就是無法移動的點,可以認為是DAG圖中的終點。從這個終點倒推,所有能夠直接連接終點的點是N點。這個很好理解,假如在當前的狀態當中,可以移動到一個對手的必敗狀態,那麼對於當前玩家當然是必勝的。
我們對這些狀態做一個簡單的分層,直接可以連接P點的點是一級勝態。比如nim遊戲當中的(1, 0)狀態就是一級勝態,它只能通往P點。我們把可以變成敗態也可以進入一級勝態的點稱為二級勝態,比如nim遊戲當中的(0, 2)。比如它進入(1, 0)便是一級勝態,而也可以直接進入(0, 0)變成敗態,我們把這樣的狀態稱為二級勝態。類似的,如果一個勝態可以變成敗態也可以變成1至n-1級的所有勝態,則稱為n級勝態,敗態可以認為是0級勝態。
接著我們來看勝態的組合,我們可以把Nim取子問題中的結論照搬過來。兩個同級的勝態組合是敗態,兩個不同級的勝態組合是勝態。原因也很簡單,就和Nim取子問題中面臨兩堆同樣多的石子必敗一樣。因為後手可以拷貝先手的操作,直到無法繼續操作,遊戲結束。而兩個不同的勝態,先手可以將其中一個轉化成和另一個一樣,從而讓後手面臨兩個一樣的勝態,所以不同的勝態組合是勝態。
注意這個結論要成立,有兩個前提條件,第一個是一個n級勝態可以轉移到1 – n-1級任意勝態。第二個是勝態級數只能降低不能升高,其實能升高也沒問題,後手可以將先手升高的級數再降低回去,並不會影響結論。
如果你理解了這一層,其實我們對級數的定義其實就是SG函數的值。SG函數能夠用來將一個狀態映射成一個非負整數的值。它的公式寫成這樣:
式子中的A和B表示狀態,表示A狀態可以達到B狀態。mex是一個定義在集合上的函數,返回的是不屬於這個集合的最小非負整數。比如mex(0, 1)= 2,mex(0, 2) = 1, mex(0,1, 2, 3)=4。也就是說我們可以通過A能達到的狀態的SG函數值推算出A的SG值。
比如在Nim取子問題當中,沒有石子是必敗,它沒有後繼狀態,所以SG(0) = 0。1顆石子的時候可以移動到0,所以SG(1) = mex{SG(0)} = 1。這樣我們就把Nim取子問題和ICG問題當中勝態的級數對應起來了。一個SG數對應Nim當中的一個石子堆,如果我們有多個石子堆,我們怎麼計算開始時候的勝負狀態?通過Nim取子問題的推導,我們知道是計算各個石子堆石子數量的亦或值,如果亦或之後的結果為0,那麼先手必敗,否則先手必勝。同樣我們計算所有狀態的SG值,如果最後得到的SG值亦或的結果為0,說明先手必敗,否則先手必勝。
關於SG(A + B) = SG(A) xor SG(B)我們可以用數學歸納法來證明,但這個證明沒什麼必要,我們更重要的是理解SG函數的思想和推導過程。最後,我們利用SG函數來看一道例題。
例題實戰
我們來看一道改進版的Nim取子問題,假設有n堆石子,每堆當中有若干個石子。現在兩個人輪流從其中取石子,一個人可以選擇從任意一堆石子當中取走若干個石子,或者是選擇一堆大於1顆石子的石堆將它拆分成兩堆。最後無法取走石子的人落敗,請問給定每堆石子的數量,誰會獲勝。
這題如果去除掉石堆可以拆分的限制,那麼它就是一道裸的Nim取子問題。但是加上了限制之後,我們就無法直接使用Nim取子的規則來求解了。但是我們分析一下會發現,這個雖然加上了限制條件,但是仍然滿足ICG遊戲的限制,所以我們可以使用SG函數來求解。
對於狀態N來說,它可以轉移到0-N-1任意狀態,並且可以拆分成i和N-i兩個狀態。根據上文的公式:SG(A+B) = SG xor SG(B),所以我們SG(N)而言,它可以轉移到0-N-1狀態,**以及(i, N-i)**狀態。
根據狀態之間的關係,我們可以很容易寫出求解SG函數的程式碼:
sg_arr = [0 for _ in range(50)]
def mex(nums):
# 返回第一個不在nums當中的自然數
if len(nums) == 0:
return 0
for i in range(1, len(nums)+1):
if i not in nums:
return i
return len(nums) + 1
def sg(n):
sgs = []
# 記錄下0-N-1狀態
for i in range(n):
sgs.append(sg_arr[i])
# N也可以達到(i, N-i)狀態,SG(i, N-i) = SG(i) ^ SG(N-i)
for i in range(1, n):
ret = sg_arr[i] ^ sg_arr[n-i]
if ret not in sgs:
sgs.append(ret)
# 通過mex函數求解出sg[n]
sg_arr[n] = mex(sorted(sgs))
print(sg_arr[n])
由於我們計算後面的SG值也需要用到前面的SG值,所以我們需要將之前的答案存儲在數組當中。否則的話,如果用遞歸執行的話會非常耗時。實際上我們計算每一個數的SG值本身也非常耗時,尤其是N很大的時候。所以這個時候可以採取取巧的辦法,就是打出一些狀態的SG值來進行觀察,尋找其中的規律。
打表找規律這種方法不甚高明,但是在比賽當中經常使用。
我們列印出一些SG值之後,可以發現對於n而言,如果n % 4 == 0,那麼SG(n) = n-1, n % 4 == 3, SG(n) = n+1,否則SG(n) = n。
利用這個規律,我們可以直接得到SG值,把每堆石子的SG值亦或在一起就是最終的答案了。
總結
到這裡,我們關於博弈論當中SG函數值的使用就介紹完了。雖然我們用了很多筆墨去說明其中的原理,但是對於初學者而言,估計還是蒙圈的。這是非常正常的,博弈論問題本身就比較考驗思維,加上SG函數的推導過程也不是很直觀,初學會覺得燒腦和想不明白也是肯定的。
有一個技巧是抓住SG值和Nim取子遊戲這個模型的對應關係,從Nim遊戲入手,會簡單一些。實際上SG值最初也的確是從Nim取子遊戲當中推導出來的。
理解了SG函數之後,就足夠我們解決絕大多數博弈論演算法問題了。這也是博弈論領域非常重要的概念和方法,希望大家都能理解。
今天的文章就到這裡,如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。
本文使用 mdnice 排版