Nim遊戲及其相關拓展

        最初瞭解博弈相關的一些東西是在暑假集訓的時候學長介紹的,當時是主講DP時順帶談到了一點博弈內容。最近對於一些組合遊戲的問題稍微多看了一點,參考其餘博文和查閱資料,發現有挺多有意思的東西,如下即是一些相關內容學習摘要和介紹。html

Nim遊戲模型兩人取石子博弈,規則以下:有若干堆石子,每堆石子的數量都是有限的,合法的移動是「選擇一堆石子並拿走若干顆(不能不拿)」,若是輪到某我的時全部的石子堆都已經被拿空了,則判負(由於他此刻沒有任何合法的移動)。算法

       從簡單的例子先來分析,若是隻有兩堆石子,那麼應該能比較容易想到必勝策略:即當兩堆石子的數量相同時,那麼先手必敗,或者說後手有必勝策略(固然有個前提就是這兩我的都必須是足夠聰明,知道遊戲策略的人)。由於取石子只能取一堆中的若干,不能同時取不一樣堆的石子,因此後手的那我的徹底能夠根據先手者的取法來決定本身的取法(先手者取多少個,後者只要在另一堆跟着取多少個就行)。例若有兩堆都是隻有3個石子的石子堆,用(3,3)表示初始狀態,其中有一種狀態過程能夠這樣表示:(3,3)->(2,3)->(2,2)->(0,2)->(0,0)。固然還有不少不一樣的狀態,但最終都是後手贏。那麼反之,當兩堆石子不一樣的時候,那麼先手擁有必勝策略,後手必敗。結果比較容易分析,再也不詳細敘述。數組

  那麼若是問題變得複雜一點,不是兩堆石子,是三堆或者更多堆呢?這時咱們就要分析一套能得出必勝策略的算法來解決這個問題。函數

  定義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。好比以前分析的當只有兩堆石子時,當着兩堆石子的石子數相等時後手有必勝策略,也便是一個P-position。以前提到的(3,3)就是一個P-position,而且,(3,3)的全部子局都是N-position。經過一點簡單的數學概括,能夠嚴格的證實「有兩堆石子時的局面是P-position當且僅當這兩堆石子的數目相等」。學習

  根據上面這個過程,能夠獲得一個遞歸的算法——對於當前的局面,遞歸計算它的全部子局面的性質,若是存在某個子局面是P-position,那麼向這個子局面的移動就是必勝策略。固然,可能你已經敏銳地看出有大量的重疊子問題,因此能夠用DP或者記憶化搜索的方法以提升效率。但問題是,利用這個算法,對於某個Nim遊戲的局面(a1,a2,...,an)來講,要想判斷它的性質以及找出必勝策略,須要計算O(a1*a2*...*an)個局面的性質,無論怎樣記憶化都沒法下降這個時間複雜度。因此咱們須要更高效的判斷Nim遊戲的局面的性質的方法。spa

  那麼如何從兩堆擴展到多堆的必勝策略算法。首先來回憶一下,每一個正整數都有對應的一個二進制數,例如:57(10) à 111001(2) ,即:57(10)=25+24+23+20。因而,咱們能夠認爲每一堆硬幣數由2的冪數的子堆組成。這樣,含有57枚硬幣大堆就能當作是分別由數量爲25、24、23、20的各個子堆組成。code

如今考慮各大堆大小分別爲N 1,N 2,……N k的通常的Nim取子游戲。將每個數N i表示爲其二進制數(數的位數相等,不等時在前面補0):
N 1 = a s…a 1a 0
N 2 = b s…b 1b 0
……
N k = m s…m 1m 0
若是每一種大小的子堆的個數都是偶數,咱們就稱Nim取子游戲是平衡的,而對應位相加是偶數的稱爲平衡位,不然稱爲非平衡位。所以,Nim取子游戲是平衡的,當且僅當:

as + bs + … + ms 是偶數htm

……

a1 + b1 + … + m1 是偶數blog

a0 + b0 + … + m0是偶數排序

因而,咱們就能得出獲勝策略:遊戲人I可以在非平衡取子游戲中取勝,而遊戲人II可以在平衡的取子游戲中取勝。

或者按以前的P-position和N-position的狀態來給出結論的話也即:對於一個Nim遊戲的局面(a1,a2,...,an),它是P-position當且僅當a1^a2^...^an=0,其中^表示異或(xor)運算。

  結論和異或運算有關,仔細分析能夠發現這個異或運算其實就是上述描述平衡與非平衡狀態的一種表示方式。下面給出這個結論的證實:根據定義,證實一種判斷position的性質的方法的正確性,只需證實三個命題: 一、這個判斷將全部terminal position判爲P-position;二、根據這個判斷被判爲N-position的局面必定能夠移動到某個P-position;三、根據這個判斷被判爲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問題就這樣基本上完美的解決了。

   那麼對於N堆的Nim模型遊戲咱們已經找到了快速的解決方法。但更擴展一點,當Nim遊戲規則稍微變更一下,每次最多隻能取K個,或者說每次取的個數是已知而且給定的一些數呢,那麼又該怎麼找出必勝策略呢?

  下面是本身查閱的一些資料的彙總,粘貼其餘人的博客(詳見http://www.cnblogs.com/frog112111/p/3199780.html)首先是引出一個SG函數。定義:sg(x) = mex ( sg(y) |y是x的後繼結點 ),其中mex(x)(x是一個天然是集合)函數是x關於天然數集合的補集中的最小值,好比x={0,1,2,4,6} 則mex(x)=3。

什麼是後繼結點?

所謂後繼結點就是當前結點通過一個操做能夠變成的狀態。好比對於取石子游戲,假如每次能夠取的數目是1,2,4,當前的石子數目也就是當前狀態是5,那麼5的後繼結點就是{5-1, 5-2, 5-4}={4,3,1};若是5的三個後繼結點的SG函數值分別爲0,1,3,那麼5的SG值就是集合{0,1,3}的補集的最小元素,也就是2。

再好比:取石子問題,有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....

計算從1-n範圍內的SG值。

f(存儲能夠走的步數,f[0]表示能夠有多少種走法)

f[]須要從小到大排序

1.可選步數爲1~m的連續整數,直接取模便可,SG(x) = x % (m+1);

2.可選步數爲任意步,SG(x) = x;

3.可選步數爲一系列不連續的數,用GetSG()計算

貼一份網上找的GetSG()模板代碼:

 模板一:

//f[]:能夠取走的石子個數
//sg[]:0~n的SG函數值
//hash[]:mex{}
int f[N],sg[N],hash[N];     
void getSG(int n)
{
    int i,j;
    memset(sg,0,sizeof(sg));
    for(i=1;i<=n;i++)
    {
        memset(hash,0,sizeof(hash));
        for(j=1;f[j]<=i;j++)
            hash[sg[i-f[j]]]=1;
        for(j=0;j<=n;j++)    //求mes{}中未出現的最小的非負整數
        {
            if(hash[j]==0)
            {
                sg[i]=j;
                break;
            }
        }
    }
}

 模板二(dfs):

//注意 S數組要按從小到大排序 SG函數要初始化爲-1 對於每一個集合只需初始化1遍
//n是集合s的大小 S[i]是定義的特殊取法規則的數組
int s[110],sg[10010],n;
int SG_dfs(int x)
{
    int i;
    if(sg[x]!=-1)
        return sg[x];
    bool vis[110];
    memset(vis,0,sizeof(vis));
    for(i=0;i<n;i++)
    {
        if(x>=s[i])
        {
            SG_dfs(x-s[i]);
            vis[sg[x-s[i]]]=1;
        }
    }
    int e;
    for(i=0;;i++)
        if(!vis[i])
        {
            e=i;
            break;
        }
    return sg[x]=e;
}

對於不少組合遊戲,SG函數均可以運用到,很是簡單方便。

關於整個遊戲的sg值之和sum,定義sum=sg1 ^ sg2 ^ sg3 ^ ……sgn.  其中^表示按位異或運算。

結論:一個遊戲的初始局面是必敗態當且僅當sum=0。

將此結論帶到以前得出的一些結論發現還是成立的。下面給出一些OJ上的習題練習。

hdu  1848

題意:取石子問題,一共有3堆石子,每次只能取斐波那契數個石子,先取完石子者勝利,問先手勝仍是後手勝

  1. 可選步數爲一系列不連續的數,用GetSG(計算)
  2. 最終結果是全部SG值異或的結果

AC代碼以下:

#include<stdio.h>
#include<string.h>
#define N 1001
//f[]:能夠取走的石子個數
//sg[]:0~n的SG函數值
//hash[]:mex{}
int f[N],sg[N],hash[N];     
void getSG(int n)
{
    int i,j;
    memset(sg,0,sizeof(sg));
    for(i=1;i<=n;i++)
    {
        memset(hash,0,sizeof(hash));
        for(j=1;f[j]<=i;j++)
            hash[sg[i-f[j]]]=1;
        for(j=0;j<=n;j++)    //求mes{}中未出現的最小的非負整數
        {
            if(hash[j]==0)
            {
                sg[i]=j;
                break;
            }
        }
    }
}
int main()
{
    int i,m,n,p;
    f[0]=f[1]=1;
    for(i=2;i<=16;i++)
        f[i]=f[i-1]+f[i-2];
    getSG(1000);
    while(scanf("%d%d%d",&m,&n,&p)!=EOF)
    {
        if(m==0&&n==0&&p==0)
            break;
        if((sg[m]^sg[n]^sg[p])==0)
            printf("Nacci\n");
        else
            printf("Fibo\n");
    }
    return 0;
}

hdu  1536

題意:首先輸入K 表示一個集合的大小  以後輸入集合 表示對於這對石子只能取這個集合中的元素的個數

以後輸入 一個m 表示接下來對於這個集合要進行m次詢問

以後m行 每行輸入一個n 表示有n個堆  每堆有n1個石子  問這一行所表示的狀態是贏仍是輸 若是贏輸入W不然L

思路: 對於n堆石子 能夠分紅n個遊戲 以後把n個遊戲合起來就行了
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
//注意 S數組要按從小到大排序 SG函數要初始化爲-1 對於每一個集合只需初始化1遍
//n是集合s的大小 S[i]是定義的特殊取法規則的數組
int s[110],sg[10010],n;
int SG_dfs(int x)
{
    int i;
    if(sg[x]!=-1)
        return sg[x];
    bool vis[110];
    memset(vis,0,sizeof(vis));
    for(i=0;i<n;i++)
    {
        if(x>=s[i])
        {
            SG_dfs(x-s[i]);
            vis[sg[x-s[i]]]=1;
        }
    }
    int e;
    for(i=0;;i++)
        if(!vis[i])
        {
            e=i;
            break;
        }
    return sg[x]=e;
}
int main()
{
    int i,m,t,num;
    while(scanf("%d",&n)&&n)
    {
        for(i=0;i<n;i++)
            scanf("%d",&s[i]);
        memset(sg,-1,sizeof(sg));
        sort(s,s+n);
        scanf("%d",&m);
        while(m--)
        {
            scanf("%d",&t);
            int ans=0;
            while(t--)
            {
                scanf("%d",&num);
                ans^=SG_dfs(num);
            }
            if(ans==0)
                printf("L");
            else
                printf("W");
        }
        printf("\n");
    }
    return 0;
}

 諸如此類的題還有HDU1846-1850,1536,2147等等,再也不一一舉例。

 大量的組合遊戲均可以用SG函數來求解,例如另一種模型:翻硬幣的問題

問題描述:

在一條直線上排列着一行硬幣,有的正面朝上、有的背面朝上。2名遊戲者輪流對硬幣進行翻轉。翻轉時,先選一枚正面朝上的硬幣翻轉,而後,若是願意,能夠從這枚硬幣的左

邊選取一枚硬幣翻轉。最後翻轉使全部硬幣反面朝上的玩家勝利。

1  2  3  4  5  6  7   8  9  10 11 12 13

T  H  T  T  H  T  T  H  T   H   H   T  H

分析:基於轉化的思想,若是將位置i上的H看做一堆規模爲i的石子,咱們就能看到遊戲與nim的類似點。不過有許多nim中的合法操做在這個遊戲中是沒法實現的,好比圖一狀態中,咱們就沒法從一堆規模爲10的石子中取出其中的5個。不過咱們將10與5同時翻轉,所獲得的狀態。對應的nim狀態SG值與從10中取5是相同的。所以咱們猜測這個遊戲的狀態SG函數值與對應的nim相同。因爲事實上雖然這個遊戲沒法實現nim的全部操做,但它的狀態所能到達的狀態SG函數值集合與對應的nim遊戲狀態相同。所以,它的狀態SG函數值與對應nim相同。

每一個硬幣的SG值爲它的編號。

若是對於一個局面,把正面硬幣的SG值異或起來不等於0,既a1^a2^a3^…^an==x,對於an來講必定有an'=an^x<an。

若是an'==0,意思就是說,把an這個值從式子中去掉就能夠了。對應遊戲,就是把編號爲an的正面硬幣翻成背面就能夠了。

若是an'!=0,意思就是說,把an這個值從式子中去掉後再在式子中加上an',an'<an。對應遊戲,去掉an就是把編號爲an的正面硬幣翻成背面,加上an',若是編號爲an'的硬幣是正面,咱們就把它翻成背面,是背面就翻成正面,總之,就是翻轉編號爲an'的硬幣。

  參考:http://blog.sina.com.cn/s/blog_51cea4040100h3wl.html

相關文章
相關標籤/搜索