1. 程式人生 > >博弈論 Nim遊戲與SG函式

博弈論 Nim遊戲與SG函式

普通Nim遊戲:
有若干堆石子,兩人輪流從中取石子,取走最後一個石子的人為勝利者
我們判斷先手必勝還是先手必敗就要判斷先手面對的局面是必勝態還是必敗態
並且普通Nim遊戲滿足以下性質:
1.無法移動的狀態是必敗態
2.可以移動到必敗態的局面一定是非必敗態
3.在必敗態做所有操作的結果都是非必敗態
這些性質很好理解,就不予以證明了
接下來分析什麼情況下是必敗態
在普通Nim遊戲中,a1^a2^a3^……^an=0是必敗態
以下是證明:
從最簡單的說起,若先手面對的是0 0 0 0 0 0這種局面,那麼先手必敗(xor為0 )
如果a1^a2^……^ai^……^an=0,那麼一定不存在某個合法操作使得ai變為ai’後依然滿足a1^a2^……^ai’^……^an=0,因為xor滿足消除性質,所以可以得出a1^a2^……^ai^……^an=0=a1^a2^……^ai’^……^an==>ai=ai’

所以如果先手面對的時xor=0的情況,先手必敗,反之先手必勝
但是如果遊戲變得稍微複雜一點,比如我規定了第一堆中只能取一顆,第二堆中只能取奇數課顆……這可怎麼辦呀———-鏘鏘鏘鏘!!!SG函數出場
SG函式全稱為Sprague-Grundy 函式
現在我們來研究一個看上去似乎更為一般的遊戲:給定一個有向無環圖和一個起始頂 點上的一枚棋子,兩名選手交替的將這枚棋子沿有向邊進行移動,無法移動者判負。事實上,這個遊戲可以認為是所有Impartial Combinatorial Games(也就是Nim遊戲)的抽象模型。也就是說,任何一個ICG都可以通過把每個局面看成一個頂點,對每個局面和它的子局面連一條有向邊來抽象成這個“有向圖遊戲”。下 面我們就在有向無環圖的頂點上定義Sprague-Garundy函式。
首先定義mex(minimal excludant)運算,表示最小的不屬於這個集合的非負整數。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。
SG(x)=mex{SG(y) | x->y (y是x的後繼)},其實就是狀態x能轉移到y
對於所有的出度為0的點他們的sg函式都等於0,因為其後繼集合為空集,對於每一個SG值為0的點,其後繼中一定滿足SG(y)!=0,對於每一個SG值不為0的點,其後繼中一定存在一個SG(y)=0
以上的結論表明SG=0的點對應的是必敗態,我們通過計算有向無環圖每個頂點的SG值就可以找到必勝的策略
如果問題在複雜一點呢(⊙o⊙)…
讓我們再來考慮一下頂點的SG值的意義。當g(x)=k時, 表明對於任意一個0<=i< k,都存在x的一個後繼y滿足g(y)=i。也就是說,當某枚棋子的SG值是k時,我們可以把它變成0、變成 1、……、變成k-1,但絕對不能保持k不變。不知道你能不能根據這個聯想到Nim遊戲,Nim遊戲的規則就是:每次選擇一堆數量為k的石子,可以把它變 成0、變成1、……、變成k-1,但絕對不能保持k不變。這表明,如果將n枚棋子所在的頂點的SG值看作n堆相應數量的石子,那麼這個Nim遊戲的每個必 勝策略都對應於原來這n枚棋子的必勝策略。
所以我們可以證明當選手處於必敗態時,所有棋子所在節點的SG值xor=0
所 以我們可以定義有向圖遊戲的和(Sum of Graph Games):設G1、G2、……、Gn是n個有向圖遊戲,定義遊戲G是G1、G2、……、Gn的和(Sum),遊戲G的移動規則是:任選一個子遊戲Gi 並移動上面的棋子。就可以得到以下式子:SG(G)=SG(G1)^SG(G2)^…^SG(Gn)。

如果你看了以上一堆證明感覺頭大的話,我們來換一種小清新萌萌噠的證明方法:
考慮一個經常會遇到的問題:兩個遊戲的和。有兩個遊戲A和B,兩個人玩,每個人每輪可以操作其中一個,但不能不操作,兩個遊戲都變為空集時輸。
定義運算子+
A + B = {X + B | X ∈ A} ∪ {A + Y | Y ∈ B}
這是個遞迴定義。
於是我們來考慮SG(A + B)
容易知道:
SG(A + B) = mex({SG(X + B) | X ∈ A} ∪ {SG(A + Y) | Y ∈ B})
現在到了這裡,大膽猜想吧……
經過數學家的一番折騰,發現SG(A + B) = SG(A) ^ SG(B)
其中x ^ y指x和y的異或值。

然後來考慮複雜問題:
取石子問題,有1堆n個的石子,每次只能取{1,3,4}個石子,先取完石子者勝利,那麼各個數的SG值為多少?
sg[0]=0,f[]={1,3,4},
x=1時,可以取走1-f{1}個石子,剩餘{0}個,mex{sg[0]}={0},故sg[1]=1;
x=2時,可以取走2-f{1}個石子,剩餘{1}個,mex{sg[1]}={1},故sg[2]=0;
x=3時,可以取走3-f{1,3}個石子,剩餘{2,0}個,mex{sg[2],sg[0]}={0,0},故sg[3]=1;
x=4時,可以取走4-f{1,3,4}個石子,剩餘{3,1,0}個,mex{sg[3],sg[1],sg[0]}={1,1,0},故sg[4]=2;
x=5時,可以取走5-f{1,3,4}個石子,剩餘{4,2,1}個,mex{sg[4],sg[2],sg[1]}={2,0,1},故sg[5]=3;
以此類推…..
x 0 1 2 3 4 5 6 7 8….
sg[x] 0 1 0 1 2 3 2 0 1….
好暈啊+_+ T_T
不管了,反正我們可以得出以下性質
1.可選步數為1~m的連續整數,直接取模即可,SG(x) = x % (m+1);
2.可選步數為任意步,SG(x) = x;
3.可選步數為一系列不連續的數,用GetSG()計算
例題:BZOJ 1874
如果先手必勝則SGaixor!=0
否則先手必敗,如果要知道第一步操作就暴力列舉
程式碼如下:

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int maxn=1000+5;
int n,m,a[10+5],b[10+5],SG[maxn],vis[10+5];
void GETSG(int N){
    for(int i=1;i<=N;i++){
        memset(vis,0,sizeof(vis));
        for(int j=1;b[j]<=i&&j<=m;j++)
            vis[SG[i-b[j]]]=1;//i的後繼狀態即為 i-b[j],SG[i-b[j]]為後繼狀態到不了的狀態,所以那個狀態i也到不了 
        for(int j=0;j<=10;j++)//求mes中未出現的最小的自然數
            if(vis[j]==0){
                SG[i]=j;
                break;
            } 
    }
}
inline int read(){
    char ch=getchar();
    int f=1,x=0;
    while(!(ch>='0'&&ch<='9')){
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9')
        x=x*10+ch-'0',ch=getchar();
    return f*x;
}
signed main(void){
    n=read();
    for(int i=1;i<=n;i++)
        a[i]=read();
    m=read();
    for(int i=1;i<=m;i++)
        b[i]=read();
    GETSG(1000);
    int ans=0;
    for(int i=1;i<=n;i++)
        ans^=SG[a[i]];
    if(!ans){
        cout<<"NO"<<endl;
        return 0;
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m&&b[j]<=a[i];j++)
            if((ans^SG[a[i]])==SG[a[i]-b[j]]){
                cout<<"YES"<<endl<<i<<" "<<b[j]<<endl;
                return 0;
            }
    }
}

Anti-Nim BZOJ 1022
小約翰經常和他的哥哥玩一個非常有趣的遊戲:桌子上有n堆石子,小約翰和他的哥哥輪流取石子,每個人取的時候,可以隨意選擇一堆石子,在這堆石子中取走任意多的石子,但不能一粒石子也不取,我們規定取到最後一粒石子的人算輸。小約翰相當固執,他堅持認為先取的人有很大的優勢,所以他總是先取石子,而他的哥哥就聰明多了,他從來沒有在遊戲中犯過錯誤。小約翰一怒之前請你來做他的參謀。自然,你應該先寫一個程式,預測一下誰將獲得遊戲的勝利。
分析:
我們首先考慮最簡單的情況–全都是1
如果有奇數堆,顯然先手必敗
偶數堆先手必勝
然後考慮下一種情況:
1 1 1 1 1 ……>1
先手必勝,為什麼呢
如果前面有奇數個1,那麼先手吧最後一堆取完,變成奇數個1,則後手必敗
如果前面有偶數個1,那麼先手把最後一堆取為1個,變成奇數堆1,後手必敗
在這種情況下,sg函式也就是xor和≠0,先手必勝
如果能夠保證每次先手取完之後xor=0那麼先手必勝(因為後手不管怎麼去sg函式一定不等於0,最後一定可以取乘1 1 1 1……>1的形式)
程式碼如下:

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int maxn=5000+5;
int cas,n,cnt,flag,ans;
inline int read(){
    char ch=getchar();
    int f=1,x=0;
    while(!(ch>='0'&&ch<='9')){
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9')
        x=x*10+ch-'0',ch=getchar();
    return f*x;
}
signed main(void){
    cas=read();
    while(cas--){
        n=read(),flag=1,ans=0;
        for(int i=1,x;i<=n;i++){
            x=read();
            if(x!=1)
                flag=0;
            ans^=x;
        }
        if(flag){
            if(n&1)
                cout<<"Brother"<<endl;
            else
                cout<<"John"<<endl;
        }
        else{
            if(ans==0)
                cout<<"Brother"<<endl;
            else
                cout<<"John"<<endl;
        }
    }
    return 0;
}