博弈學習(一) NIM + SG函式

bigbigship發表於2015-03-27
(一).NIM博弈

1)遊戲規則

通常的Nim遊戲的定義是這樣的:有若干堆石子,每堆石子的數量都是有限的,合法的移動是“選擇一堆石子並拿走若干顆(不能不拿)”,如果輪到某個人時所有的石子堆都已經被拿空了,則判負(因為他此刻沒有任何合法的移動)。


2)分析
定義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,而且可以通過定義計算出來。

3)結論
根據定義,證明一種判斷position的性質的方法的正確性,只需證明三個命題: 1、這個判斷將所有terminal position判為P-position;2、根據這個判斷被判為N-position的局面一定可以移動到某個P-position;3、根據這個判斷被判為P-position的局面無法移動到某個P-position。
第一個命題顯然,terminal position只有一個,就是全0,異或仍然是0。
第二個命題,對於某個局面(a1,a2,...,an),若a1^a2^...^an<>0,一定存在某個合法的移動,將ai改變成ai'後滿足a1^a2^...^ai'^...^an=0。不妨設a1^a2^...^an=k,則一定存在某個ai,它的二進位制表示在k的最高位上是1(否則k的最高位那個1是怎麼得到的)。這時ai^k<ai一定成立。則我們可以將ai改變成ai'=ai^k,此時a1^a2^...^ai'^...^an=a1^a2^...^an^k=0。
第三個命題,對於某個局面(a1,a2,...,an),若a1^a2^...^an=0,一定不存在某個合法的移動,將ai改變成ai'後滿足a1^a2^...^ai'^...^an=0。因為異或運算滿足消去率,由a1^a2^...^an=a1^a2^...^ai'^...^an可以得到ai=ai'。所以將ai改變成ai'不是一個合法的移動。證畢。
根據這個定理,我們可以在O(n)的時間內判斷一個Nim的局面的性質,且如果它是N-position,也可以在O(n)的時間內找到所有的必勝策略。Nim問題就這樣基本上完美的解決了。

(二) SG函式

1)mex運算
首先定義mex(minimal excludant)運算,這是施加於一個集合的運算,表示最小的不屬於這個集合的非負整數。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。
 
對於一個給定的有向無環圖,定義關於圖的每個頂點的Sprague-Garundy函式g如下:g(x)=mex{ g(y) | y是x的後繼 }。

2)SG函式的性質
首先,所有的terminal position所對應的頂點,也就是沒有出邊的頂點,其SG值為0,因為它的後繼集合是空集。然後對於一個g(x)=0的頂點x,它的所有後繼y都滿足 g(y)!=0。對於一個g(x)!=0的頂點,必定存在一個後繼y滿足g(y)=0。
SG(X)=0:代表當前狀態為必敗狀態


(三)練習題:


problem one: HDU 4848 Fibonacci again and again

分析:

nim 博弈的簡單變形,只需要預處理出(1,1000)的數的SG函式的值即可。

程式碼如下:

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

const int maxn = 1010;

int f[30];
int sg[maxn+1];
int vis[maxn+1];

void init(){
    f[0]=1,f[1]=1;
    for(int i=2;i<20;i++)
        f[i]=f[i-1]+f[i-2];
}

void solve(){
    init();
    memset(sg,0,sizeof(sg));
    for(int i=1;i<=1000;i++){
        memset(vis,0,sizeof(vis));
        for(int j=1;j<maxn&&f[j]<=i;j++)
            vis[sg[i-f[j]]]=1;
        for(int j=0;;j++){
            if(!vis[j]){
                sg[i]=j;
                break;
            }
        }
    }
}

int main()
{
    int n,m,p;
    solve();
    while(~scanf("%d%d%d",&n,&m,&p)){
        if(n==0&&m==0&&p==0)
            break;
        if(sg[n]^sg[m]^sg[p])
            puts("Fibo");
        else
            puts("Nacci");
    }
    return 0;
}

problem two:HDU 1907 John

分析:

nim博弈,注意全部為1的情況。

程式碼如下:

Code Render Status : Rendered By HDOJ C++ Code Render Version 0.01 Beta

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

const int maxn = 50;

int a[maxn];

int main()
{
    int t,n;
    scanf("%d",&t);
    while(t--){
        scanf("%d",&n);
        int s=0,num=0;
        for(int i=0;i<n;i++){
            scanf("%d",&a[i]);
            s^=a[i];
            if(a[i]>1) num++;
        }
        if((s&&num)||(!s&&!num))
           puts("John");
        else
            puts("Brother");
    }
    return 0;
}


problem there :HDU 3032 Nim or not Nim?

分析:

nim博弈的變形,每次可以從一堆取若干,或者把一堆變成兩堆。打表找SG函式的規律。

程式碼如下:

打表找規律的程式碼:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

const int maxn = 1000010;

int a[maxn];

int sg[maxn];

int get(int x){
    int vis[1000];
    memset(vis,0,sizeof(vis));
    if(sg[x]!=-1) return sg[x];
    for(int i=x-1;i>=0;i--)
        vis[get(i)]=1;
    for(int i=1;i<=x/2;i++){
        int ans = 0;
        ans^=get(i);
        ans^=get(x-i);
        vis[ans]=1;
    }
    for(int i=0;;i++){
        if(!vis[i]){
            sg[x]=i;
            break;
        }
    }
    return sg[x];
}

int main()
{
    memset(sg,-1,sizeof(sg));
    sg[0]=0;
    int n;
    while(~scanf("%d",&n)){
        for(int i=0;i<20;i++){
            printf("sg( %d ) = %d\n",i,get(i));
        }
    }
    return 0;
}

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

int get(int x){
    if(x==0) return 0;
    if(x%4==0)
        return x-1;
    if(x%4==3)
        return x+1;
    return x;
}

int main()
{
    int t,n,x;
    scanf("%d",&t);
    while(t--){
        scanf("%d",&n);
        int ans = 0;
        for(int i=0;i<n;i++){
            scanf("%d",&x);
            ans^=get(x);
        }
        if(ans) puts("Alice");
        else puts("Bob");
    }
    return 0;
}







相關文章