4.1.6 Grundy數-硬幣遊戲2
Problem Description:
Alice 和 Bob 在玩一個遊戲。給定 k 個數字 a1,a2,……,ak。一開始,有n堆硬幣,每堆各有 Xi 枚硬幣。Alice 和 Bob 輪流選出一堆硬幣,從中取出一些硬幣。每次所選硬幣的枚數一定要在 a1,a2,……,ak 當中。Alice先取,取光硬幣的一方獲勝。當雙方都採取最優策略時,誰會獲勝?題目保證a1,a2……中一定有1.
1<=n<=1000000
1<=k<=100
1<=Xi,ai<=10000
Input:
n=3
k=3
a={1,3,4}
x={5 ,6,7}
Output:
Alice
這和4.1.1中介紹的硬幣問題類似,但那道題中只有一堆硬幣,而本題中有n堆。如果依然用動態規劃演算法的話,狀態數將高達O(X1*X2*……*Xn)。
為了更高效地求解這個問題,要了解一下Grundy值這一重要概念。利用它,不光是這個遊戲,其他許多遊戲都可以轉換成前面所介紹的Nim。
讓我們再來考慮一下只有一堆硬幣的情況。硬幣枚數所對應的Grundy值的計算方法如下。
int grundy(int x){ S={}; for(i=1,……,k){ if(a_i<=x)View Code//將Grundy(x-a_i)加到S中 } return //最小的不屬於S的非負整數 }
也就是說,當前狀態的Grundy值就是除任意一步所能轉移到的狀態的Grundy值以外的最小非負整數。這樣的Grundy值,和Nim中的一個石子堆類似,有如下性質。
Nim中有x顆石子的石子堆,能夠轉移成0,1,……,x-1顆石子的石子堆;
從Grundy值為x的狀態出發,可以轉移到Grundy值為0,1,……,x-1的狀態;
只不過,與Nim不同的是轉移後的Grundy值也有可能增加。不過,對手總能選取合適的策略再轉移回相同Grundy值的狀態,所以對勝負沒有影響。(但是,對於狀態可能有迴圈時,需要注意不分勝負·達成平局(遊戲不會結束)的情況。因為在這個遊戲中,石子數始終是減少的,所以不會發生平局)
另外,上面的程式是用單純的遞迴函式實現的,改成動態規劃或記憶化搜尋之後,就能夠保證求解的複雜度為O(xk)。
瞭解了一堆硬幣的Grundy值的計算方法之後,就可以將它看作Nim中的一個石子堆。Nim中為什麼用如下方法判斷勝負。
所有石子堆的石子數Xi的XOR
X1 XOR X2 XOR …… XOR Xk
為零則必敗,否則必勝
Grundy值等價於Nim中的石子數,所以對於Grundy值的情況,有
所有硬幣堆的Grundy值的XOR
grundy(X1) XOR grundy(X2) XOR ……XOR grundy(Xk)
為零則必敗,否則必勝
不光是這個遊戲,在許多遊戲中,都可以根據“當前狀態的Grundy值等於除任意一步所能轉移到的狀態的Grundy值以外的最小非負整數”這一性質,來計算Grundy值,再根據XOR來判斷勝負。
//輸入 int N,K,X[MAX_N],A[MAX_K]; //利用動態規劃計算Grundy值的陣列 int grundy[MAX_N+1]; void solve(){ //輪到自己時剩0枚則必敗 grundy[0]=0; //計算grundy值 int max_x= *max_element(X,X+N); for(int j=1;j<max_x;j++){ set<int> s; for(int i=0;i<K;i++) if(A[i]<=j) s.insert(grundy[j-A[i]]); int g=0; while(s.count(g)!=0) g++; grund[j]=g; } //判斷勝負 int x=0; for(int i=0;i<N;i++) x^=grundy[x[i]]; if(x) puts("Alice"); else puts("Bob"); }