1. 程式人生 > >NIM遊戲,NIM遊戲變形,威佐夫博弈以及巴什博奕總結

NIM遊戲,NIM遊戲變形,威佐夫博弈以及巴什博奕總結

經典NIM遊戲:

  

一共有N堆石子,編號1..n,第i堆中有個a[i]個石子。

每一次操作Alice和Bob可以從任意一堆石子中取出任意數量的石子,至少取一顆,至多取出這一堆剩下的所有石子。

兩個人輪流行動,取走最後一個的人勝利。Alice為先手。

我們定義:

P:表示當前局面下先手必敗

N:表示當前局面下先手必勝

N,P狀態的轉移滿足如下性質:

1.合法操作集合為空的局面為P

2.可以移動到P的局面為N,這個很好理解,以為只要能轉換到P局面,那麼先手只需要使操作後變成P局面,那麼後手就面臨了一個必敗的狀態。

3.所有移動只能到達N的局面為P。無論怎麼選取都會留給對手一個必勝狀態。

其實知道這個之後應該是可以記憶化搜尋或者用sg函式求解的,但是如果範圍非常大,就沒法做了。

就引進了nim遊戲一個很神奇的結論:對於一個局面,當且僅當a[1] xor a[2] xor ...xor a[n]=0時,該局面為P局面,即必敗局面。

證明如下:

1.全0的局面一定是P局面。

2.從任意一個異或值不為0(設為K)的局面一定可以轉移到一個異或值為0的狀態。由於異或計算的特殊性,我們知道一定有一個a[i]的某一位與k的最高位的1是相同的,那麼必然有a[i] xor k<a[i],我們可以通過改變a[i[的值為a[i]',使a[1] xor a[2] xor a[i] xor ...xor a[n]=0

3.對於任意一個局面,若異或值為0,則不存在任何一個移動可以使新的局面的異或值為0.如果一位的異或值為0,那麼這一位上一定有偶數個1,那麼只改變一個數,一定無法使其保持0

Moore’s Nim:

n堆石子,每次從不超過k堆中取任意多個石子,最後不能取的人失敗。

這是一個nim遊戲的變形,也是有結論:把n堆石子的石子數用二進位制表示,統計每個二進位制位上1的個數,若每一位上1的個數mod(k+1)全部為0,則必敗,否則必勝。

證明如下

1.全為0的局面一定是必敗態。

2.任何一個P狀態,經過一次操作以後必然會到達N狀態:在某一次移動中,至少有一堆被改變,也就是說至少有一個二進位制位被改變。由於最多隻能改變k堆石子,所以對於任何一個二進位制位,1的個數至多改變k。而由於原先的總數為k+1的整數倍,所以改變之後必然不可能是k+1的整數倍。故在P狀態下一次操作的結果必然是N狀態。

3.任何N狀態,總有一種操作使其變化成P狀態。從高位到低位考慮所有的二進位制位。假設用了某種方法,改變了m堆,使i為之前的所有位都回歸到k+1的整數倍。現在要證明總有一種方法讓第i位也恢復到k+1的整數倍。

有一個比較顯然的性質,對於那些已經改變的m堆,當前位可以自由選擇1或0.

設除去已經更改的m堆,剩下堆i位上1的總和為sum

分類討論:

(1)sum<=k-m,此時可以將這些堆上的1全部拿掉,然後讓那m堆得i位全部置成0.

(2)sum>k-m 此時我們在之前改變的m堆中選擇k+1-sum堆,將他們的第i位設定成1。剩下的設定成0.由於k+1-sum<k+1-(k-m)<m+1,也就是說k+1-sum<=m,故這是可以達到的;

anti-nim反nim遊戲

正常的nim遊戲是取走最後一顆的人獲勝,而反nim遊戲是取走最後一顆的人輸。

 

一個狀態為必勝態,當且僅當:

  1)所有堆的石子個數為1,且NIM_sum(xor和)=0

  2)至少有一堆的石子個數大於1,且 NIM_sum(xor和)≠0

題目:bzoj 1022: [SHOI2008]小約翰的遊戲John

#include<bits/stdc++.h>
using namespace std;  
int n,m;  
int main()  
{  
    scanf("%d",&m);  
    for (int j=1;j<=m;j++){  
        scanf("%d",&n);  
        int ans=0; int pd=0;  
        for (int i=1;i<=n;i++){  
            int x; scanf("%d",&x);  
            if (x>1) pd=1;  
            ans^=x;  
        }  
        if (pd==0&&!ans) printf("John\n");  
        else if (pd==1&&ans) printf("John\n");  
        else printf("Brother\n");  
    }  
}

Staircase NIM

顧名思義就是在階梯上進行,每層有若干個石子,每次可以選擇任意層的任意個石子將其移動到該層的下一層。最後不能操作的人輸。


   階梯博弈經過轉換可以變為Nim..把所有奇數階梯看成N堆石子做nim。把石子從奇數堆移動到偶數堆可以理解為拿走石子,就相當於幾個奇數堆的石子在做Nim。
   假設我們是先手,所給的階梯石子狀態的奇數堆做Nim先手能必勝.我就按照能贏的步驟將奇數堆的石子移動到偶數堆.如果對手也是移動奇數堆,我們繼續移動奇數堆.如果對手將偶數堆的石子移動到了奇數堆..那麼我們緊接著將對手所移動的這麼多石子從那個奇數堆移動到下面的偶數堆.兩次操作後.相當於偶數堆的石子向下移動了幾個。而奇數堆依然是原來的樣子,即為必勝的狀態。就算後手一直在移動偶數堆的石子到奇數堆,我們就一直跟著他將石子繼續往下移,保持奇數堆不變。我可以跟著後手把偶數堆的石子最終移動到0,然後對手就不能移動這些石子了.所以整個過程.將偶數堆移動到奇數堆不會影響奇數堆做Nim博弈的過程..整個過程可以抽象為奇數堆的Nim博弈.
   為什麼是隻對奇數堆做Nim就可以而不是偶數堆呢?因為如果是對偶數堆做Nim,對手移動奇數堆的石子到偶數堆,我們跟著移動這些石子到下一個奇數堆。那麼最後是對手把這些石子移動到了0,我們不能繼續跟著移動,就只能去破壞原有的Nim而導致勝負關係的不確定。所以只要對奇數堆做Nim判斷即可知道勝負情況。

題目:http://poj.org/problem?id=1704

#include<bits/stdc++.h>
using namespace std;  
int m,n;  
int a[N],p[N],b[N];  
int main()  
{  
    freopen("a.in","r",stdin);  
    scanf("%d",&m);  
    for (int i=1;i<=m;i++) {  
        scanf("%d",&n); int ans=0;  
        int cnt=0;  
        for (int j=1;j<=n;j++) scanf("%d",&a[j]);  
        sort(a+1,a+n+1);  
        for (int j=2;j<=n;j++) p[j]=a[j]-a[j-1]-1;  
        p[1]=a[1]-1;  
        for (int j=1;j<=n;j++) b[j]=p[n-j+1];  
        for (int j=1;j<=n;j++){  
            if (!b[j]) cnt++;  
            if (j&1) ans^=b[j];   
        }    
        if (cnt==n) {  
            printf("Bob will win\n");  
            continue;  
        }  
        if (ans) printf("Georgia will win\n");  
        else printf("Bob will win\n");  
    }  
  return 0; }

 

新Nim遊戲:

在第一個回合中,第一個遊戲者可以直接拿走若干個整堆的火柴。可以一堆都不拿,但不可以全部拿走。第二回合也一樣,第二個遊戲者也有這樣一次機會。從第三個回合(又輪到第一個遊戲者)開始,規則和Nim遊戲一樣。 如果你先拿,怎樣才能保證獲勝?如果可以獲勝的話,還要讓第一回合拿的火柴總數儘量小。 解法:

為使後手必敗,先手留給後手的必然是若干線性無關的數字,否則後手可以留下一個異或和為零的非空子集使得先手必敗,故問題轉化為拿走和最小的數字使得留下的數線性無關,即留下和最大的線性基,這樣拿走的數量顯然最少,找到和最大的線性基只需貪心的把數字從大到小加入到基中即可(證明需用到擬陣)

例題:BZOJ3105

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll sum;
int k,num[520],d[520];
inline int read()
{
    int x=0,f=1; char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-') f=-1; ch=getchar();}
    while(ch>='0'&&ch<='9') {x=x*10+ch-'0'; ch=getchar();}
    return x*f;
}
int Insert(int k)
{
    for(int i=31;i>=0;--i)
    {
        if(k&(1<<i))
        {
            if(!d[i]) {d[i]=k; return 1;}
            else k^=d[i];
        }
    }
    return 0;
}
bool cmp(int a,int b) {return a>b;}
 
int main()
{
    k=read();sum=0;
    for(int i=1;i<=k;++i) num[i]=read();
    sort(num+1,num+1+k,cmp);
    for(int i=1;i<=k;++i) if(!Insert(num[i])) sum+=num[i]*1ll;
    printf("%lld\n",sum);
    return 0;
}

  

威佐夫博弈:

兩堆石子,每次可以取一堆或兩堆,從兩堆中取得時候個數必須相同,先取完的獲勝。

 

這種情況下是頗為複雜的。我們用(ak,bk)(ak ≤ bk ,k=0,1,2,…,n)表示
兩堆物品的數量並稱其為局勢,如果甲面對(0,0),那麼甲已經輸了,這種局勢我們
稱為奇異局勢。前幾個奇異局勢是:(0,0)、(1,2)、(3,5)、(4,7)、(6,
10)、(8,13)、(9,15)、(11,18)、(12,20)。

    可以看出,a0=b0=0,ak是未在前面出現過的最小自然數,而 bk= ak + k,奇異局勢有
如下三條性質:

    1。任何自然數都包含在一個且僅有一個奇異局勢中。
    由於ak是未在前面出現過的最小自然數,所以有ak > ak-1 ,而 bk= ak + k > ak
-1 + k-1 = bk-1 > ak-1 。所以性質1。成立。
    2。任意操作都可將奇異局勢變為非奇異局勢。
    事實上,若只改變奇異局勢(ak,bk)的某一個分量,那麼另一個分量不可能在其
他奇異局勢中,所以必然是非奇異局勢。如果使(ak,bk)的兩個分量同時減少,則由
於其差不變,且不可能是其他奇異局勢的差,因此也是非奇異局勢。
    3。採用適當的方法,可以將非奇異局勢變為奇異局勢。

    假設面對的局勢是(a,b),若 b = a,則同時從兩堆中取走 a 個物體,就變為了
奇異局勢(0,0);如果a = ak ,b > bk,那麼,取走b  – bk個物體,即變為奇異局
勢;如果 a = ak ,  b < bk ,則同時從兩堆中拿走 ak – ab – ak個物體,變為奇異局
勢( ab – ak , ab – ak+ b – ak);如果a > ak ,b= ak + k,則從第一堆中拿走多餘
的數量a – ak 即可;如果a < ak ,b= ak + k,分兩種情況,第一種,a=aj (j < k)
,從第二堆裡面拿走 b – bj 即可;第二種,a=bj (j < k),從第二堆裡面拿走 b – a
j 即可。

    從如上性質可知,兩個人如果都採用正確操作,那麼面對非奇異局勢,先拿者必勝
;反之,則後拿者取勝。

    那麼任給一個局勢(a,b),怎樣判斷它是不是奇異局勢呢?我們有如下公式:

    ak =[k(1+√5)/2],bk= ak + k  (k=0,1,2,…,n 方括號表示取整函式)
題目:http://poj.org/problem?id=1067

#include<bits/stdc++.h>
using namespace std;
int n,m;
int main()
{
	double k=(1+sqrt(5.0))/2;
	while(scanf("%d%d",&n,&m)!=EOF) 
    {
		if (n>m) swap(n,m);
		int t=m-n; 
		if (n==(int)((double)t*k)) printf("0\n");
		else printf("1\n");
	}
    return 0;
}

  

巴什博奕

只有一堆石子共n個。每次從最少取1個,最多取m個,最後取光的人取勝。

問先手是否有必勝策略,第一步該怎麼取。

如果n=(m+1)*k+s (s!=0) 那麼先手一定必勝,因為第一次取走s個,接下來無論對手怎麼取,我們都能保證取到所有(m+1)倍數的點,那麼迴圈下去一定能取到最後一個。

題目:http://acm.hdu.edu.cn/showproblem.php?pid=1846

#include<bits/stdc++.h>
using namespace std;
int n,m;
int main()
{
	int t;
	scanf("%d",&t);
	for (int i=1;i<=t;i++){
		scanf("%d%d",&n,&m);
		if (n%(m+1)) printf("first\n");
		else printf("second\n");
	}
        return 0;        
}