1. 程式人生 > >4.1.6 Grundy數-硬幣遊戲2

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)
        
//將Grundy(x-a_i)加到S中 } return //最小的不屬於S的非負整數 }
View Code

  也就是說,當前狀態的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"); 
}