1. 程式人生 > >淺談 翻硬幣遊戲【Nim博弈】

淺談 翻硬幣遊戲【Nim博弈】

 ACM部落格_kuangbin

博弈-翻硬幣遊戲

hihoCoder

1172 : 博弈遊戲·Nim遊戲·二

時間限制:10000ms

單點時限:1000ms

記憶體限制:256MB

描述

Alice和Bob這一次準備玩一個關於硬幣的遊戲:
N枚硬幣排成一列,有的正面朝上,有的背面朝上,從左到右依次編號為1..N。現在兩人輪流翻硬幣,每次只能將一枚正面朝上的硬幣翻過來,並且可以隨自己的意願,在一枚硬幣翻轉後決定要不要將該硬幣左邊的任意一枚硬幣也翻一次(正面翻到背面或背面翻到正面)。翻最後一枚正面向上的硬幣的人獲勝。同樣的,這次遊戲裡面Alice仍然先手,兩人均採取最優的策略,對於給定的初始局面,Alice會獲勝還是Bob會獲勝?

提示:Turning Turtles

輸入

第1行:1個正整數N,表示硬幣數量。1≤N≤10,000
第2行:1個字串,第i個字元表示編號為i的硬幣狀態,’H’表示正面朝上,’T’表示背面朝上。

輸出

第1行:1個字串,若Alice能夠獲勝輸出"Alice",否則輸出"Bob"

樣例輸入

8
HHTHTTHT

樣例輸出

Bob

Turning Turtles

這個遊戲叫做Turning Turtles,它的本質就是Nim遊戲。那麼它到底是如何轉化為Nim遊戲的呢?讓我們一步一步來分析。

首先,我們先將局面分解一下,每一次我們只考慮一枚硬幣。
不妨設所有硬幣全部背面朝上的局面為局面0
假設現在N枚硬幣,只有第1枚是正面朝上的。該局面只能轉化為全部硬幣背面朝上的局面。我們假定該局面為 局面1,則局面1可以轉化為局面0。
假設只有第2枚是正面朝上的。該局面可以轉化為:只有硬幣1正面朝上;全部硬幣背面朝上。我們假定該局面為 局面2,局面2可以轉化為局面1和局面0。
同理我們可以推定,第i枚硬幣正面朝上的局面為局面i,局面i可以轉化為局面0..i-1。

現在,我們考慮把給定的局面拆成單個硬幣的局面集合

比如給定了{HHTHTTHT},其中H表示正面朝上,T表示背面朝上。那麼就是當前局面={局面1,局面2,局面4,局面7}。每一次我們可以改變其中個一個局面,當出現局面0時就從集合中刪去。
這樣一看是不是就變成了Nim遊戲了?然而事實並沒有那麼簡單。

進一步分析,若同時存在i,j(j<i)兩枚硬幣正面朝上。我們將這個局面拆成2個單一的局面:即局面i和局面j。
在反轉i的時候我們考慮從局面i轉移到局面j,那麼我們會有兩個局面j。
表示第j枚被反轉了2次,也就是回到了背面朝上的狀態。
那麼我們得到這個遊戲一個性質:當出現兩個同樣的局面時,等價於這兩個局面合併變成了局面0。

這種情況在Nim遊戲中是沒有的,那麼它會對Nim遊戲的狀態造成影響麼?
我們想一想,在Nim遊戲中,如果出現兩個數量相同的堆時,比如A[i]=A[j]。在計算Nim遊戲狀態時我們採用的xor操作,xor有交換律和結合律。則我們可以變成:
(A[i] xor A[j]) xor Other
因為A[i] = A[j],所以A[i] xor A[j] = 0。上式實際就是:
0 xor Other
也就是說在原Nim遊戲中,若出現了兩個數量相同的堆時,實際上這兩堆已經不對總局面造成影響了,也就可以認為這兩對合併為了一個數量為0的堆。

到此,我們可以發現這個硬幣遊戲完全滿足Nim遊戲的規則,其解答也滿足Nim遊戲的性質,這題也就很簡單的轉化為了普通的Nim遊戲。在實際的博弈遊戲中會發現很多都是可以轉化為Nim遊戲模型。如何正確的建立模型和轉化遊戲模型也就是解決博弈遊戲一個很重要的手段。

比如Nimble遊戲:
遊戲開始時有許多硬幣任意分佈在樓梯上,共N階樓梯從地面由下向上編號為0到N。遊戲者在每次操作時可以將任意一枚硬幣向下移動,直至地面。遊戲者輪流操作,將最後一枚硬幣移至地面(即第0階)的人獲勝。在雙方都採取最優策略的情況下,對於給定的初始局面,問先手必勝還是先手必敗。
每一枚硬幣仍然對應了一個石子堆,向下移動就等價於從石子堆裡面取出石子。

同樣的例子還有很多,有些遊戲甚至需要做好幾次轉換才能移動到Nim遊戲模型,在之後我們就會見到。

import java.util.*;
import java.math.*;
 
public class Main{
	static int MAXN=(int)(500+10);
	
	public static void main(String[] args) {
		Scanner cin=new Scanner(System.in);
		while(cin.hasNext()) {
			int n=cin.nextInt();
			String str=cin.next();
			int len=str.length();
			int ret=0;
			for(int i=0;i<len;i++) {
				if(str.charAt(i)=='H')
					ret^=(i+1);
			}
			if(ret==0)
				System.out.println("Bob");
			else
				System.out.println("Alice");
		}
		cin.close();
	}
}

HDU3537

題意:

有一排硬幣,告訴 你n個正面朝上的硬幣的位置,你可以選擇任意位置的1~3個硬幣翻轉一下,但是問你先手是否會輸。

打表找規律

#include<stdio.h>
#include<string.h>
//統計x的二進位制表示中1的個數
static int countOne(int x) {
    int ans=0;
    while(x!=0) {
        if((x&1)!=0)	ans++;
        x=x>>1;
    }
    return ans;
}
int vis[1007],sg[1007];
int main()
{
    for(int i=1;i<100;i++)
    {
        memset(vis,0,sizeof(vis));
        vis[0]=1;//翻一枚
        for(int j=0;j<i;j++)
            vis[sg[j]]=1;//翻兩枚
        for(int j=0;j<i;j++)
            for(int k=0;k<j;k++)
                vis[sg[j]^sg[k]]=1;//翻三枚
        for(int k=0;;k++)
            if(!vis[k]){sg[i]=k;break;}
        printf("%d,%d,%d\n",i-1,sg[i],countOne(i-1));
    }
}

規律:

  1. 若X的二進位制表示裡1的個數為奇數,則SG[x]=2*x;
  2. 若X的二進位制表示裡1的個數為偶數,則SG[x]=2*x+1;

最後N個X的SG值異或為0,先手必敗;

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<math.h>
#include<set>
using namespace std;
const int  maxn = 1e6+10;
typedef long long LL;
//統計x的二進位制表示中1的個數
static int countOne(int x) {
    int ans=0;
    while(x!=0) {
        if((x&1)!=0)	ans++;
        x=x>>1;
    }
    return ans;
}
//計算x的SG值
static int SG(int x) {
    int one=countOne(x);
    if(one%2==1)
        return x<<1;//sg=2*x;
    return x<<1|1;//sg=2*x+1
}
int main()
{
   int n;
   while(scanf("%d",&n)!=EOF){
        set<int>s;//自動去重
    for(int i=0;i<n;i++){
        int x;
        scanf("%d",&x);
        s.insert(x);
    }
    int ret=0;
    set<int>::iterator it;
    for(it=s.begin();it!=s.end();it++){
        //printf("%d\n",*it);
        ret^=SG(*it);
    }
    if(ret==0)
        printf("Yes\n");
    else
        printf("No\n");
   }
   return 0;
}