1. 程式人生 > >博弈之 Nim 遊戲&poj 3537 Crosses and Crosses

博弈之 Nim 遊戲&poj 3537 Crosses and Crosses

Nim遊戲的定義

Nim遊戲是組合遊戲(Combinatorial Games)的一種,準確來說,屬於“Impartial Combinatorial Games”(以下簡稱ICG)。滿足以下條件的遊戲是ICG(可能不太嚴謹):
  1. 有兩名選手;
  2. 兩名選手交替對遊戲進行移動(move),每次一步,選手可以在(一般而言)有限的合法移動集合中任選一種進行移動;
  3. 對於遊戲的任何一種可能的局面,合法的移動集合只取決於這個局面本身,不取決於輪到哪名選手操作、以前的任何操作、骰子的點數或者其它什麼因素;
  4. 如果輪到某名選手移動,且這個局面的合法的移動集合為空(也就是說此時無法進行移動),則這名選手負。

必敗必勝策略的定義

定義P-position和N-position,其中P代表Previous,N代表Next。直觀的說,上一次move的人有必勝策略的局面是P-position,也就是“後手可保證必勝”或者“先手必敗”,現在輪到move的人有必勝策略的局面是N-position,也就是“先手可保證必勝”。更嚴謹的定義是:1.無法進行任何移動的局面(也就是terminal position)是P-position;2.可以移動到P-position的局面是N-position;3.所有移動都導致N-position的局面是P-position。按照這個定義,如果局面不可能重現,或者說positions的集合可以進行拓撲排序,那麼每個position或者是P-position或者是N-position,而且可以通過定義計算出來。

例子

poj3537就是Nim遊戲的一種:
有個2人玩的遊戲在一個規模為1*n的棋盤上進行,每次一個人選擇一個地方畫上’X’,一旦某個人畫上X後出現了連續3個X,那麼這個人就贏了。給你n(3≤n≤=2000)問先手和後手誰會贏。

仔細思考一下我們發現,xxx的上一步只能是oxx,xox,xxo的其中一種,也就是說如果誰走出一步形成上述局面那麼誰就必敗。
再進一步說,如果你在第i個格子畫x,那麼i-1,i-2,i+1,i+2,都不可以畫x,因為那樣會必敗。
這樣題目就可以轉化為,輪流畫x,誰沒有格子畫x誰就輸。
以上例來進行一下計算,假設N為3,也就是OOOO,當前局面有4個子局面,XOOO,OXOO,OOXO,OOOX。
1. XOOO對於當前玩家只有一步可走,就是XOOX(別忘了上面說過每走一步,該位置左右共4個位置都不能),所以XOOO就是一個N-position局面(後手輸)。
2. OXOO對於當前玩家無步可走,所以是一個P-position(先手輸)。
3. 4. OOXO,OOOX就不再贅述,前者P,後者N。
那麼OOOO顯然是一個N-position,因為他的子局面中有P-position(他當然會把這種局面留給自己的對手)。

根據上面這個過程,可以得到一個遞迴的演算法——對於當前的局面,遞迴計算它的所有子局面的性質,如果存在某個子局面是P-position,那麼向這個子局面的移動就是必勝策略。當然,會存在大量的重疊子問題,可以用DP或者記憶化搜尋的方法以提高效率。
但問題是,利用這個演算法,對於某個Nim遊戲的局面(a1,a2,…,an)來說,要想判斷它的性質以及找出必勝策略,需要計算O(a1*a2*…*an)個局面的性質,不管怎樣記憶化都無法降低這個時間複雜度。所以我們需要更高效的判斷Nim遊戲的局面的性質的方法,也就是Bouton’s Theorem,上面的參考連結裡有這個的詳細解釋與證明,就不貼了。

總之,通過Bouton’s Theorem,可以得到,如果a的子局面a1^a2^..^an == 0,a就是一個N-position,否則a是P-position。
這也解清了我對好多人用異或的一個疑惑。

程式碼

#include<iostream>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<algorithm>
#include<stack>
#include<queue>
using namespace std;

const int N = 2009;
int sg[N];

int getsg(int n)
{
    if(n<=0)
        return 0;
    if(sg[n] != -1)
        return sg[n];
    bool f[N] = {};//對當前局面的子局面進行標記
    for(int i=1; i<=n; i++)
        f[getsg(i-3)^getsg(n-i-2)] = 1;//標記所有的子局面
    for(int i=0; ; i++)//如果子局面沒有0,則說明當前局面為P-position。
        if(!f[i])
            return sg[n] = i;
}

int main()
{
    int n;
    while(cin>>n)
    {
        memset(sg, -1, sizeof(sg));
        if(getsg(n))
            cout<<"1"<<endl;
        else
            cout<<"2"<<endl;
    }
    return 0;
}