以前寫的關於博弈基礎知識的博客:基礎博弈論【巴什博弈、威佐夫博弈、尼姆博弈、反尼姆博弈】https://blog.csdn.net/ljw_study_in_CSDN/article/details/88356973
先複習一些概念:
- 先手必勝爲N-position,後手必勝爲P-position。
- 必敗點(P態):前一個選手(Previous player)將取勝的位置稱爲必敗點。
- 必勝點(N態) :下一個選手(Next player)將取勝的位置稱爲必勝點。
- 現在關於P,N的求解有三個規則
(1):最終態都是P(遊戲規則是:最後不能進行操作的人輸)
(2):按照遊戲規則,到達當前態的前態都是N的話,則當前態是P
(3):按照遊戲規則,到達當前態的前態至少有一個P的話,則當前態是N
———————————————————————————————————————————————————————
hdu 1730 Northcott Game
尼姆博弈。
爲什麼是Nim博弈呢?通常的Nim遊戲的定義是這樣的:有若干堆石子,每堆石子的數量都是有限的,合法的移動是“選擇一堆石子並拿走若干顆(不能不拿)”,如果輪到某個人時所有的石子堆都已經被拿空了,則判負(因爲他此刻沒有任何合法的移動)。
先手從某一堆中取x個石子然後後手取y個石子,直到這一堆中沒有石子和黑棋走x步然後白棋走y步直到黑白棋相遇(如果黑白棋子相遇,那先手必輸,因爲先手如果移動,後手緊跟移動,直至先手不能動,故先手必輸)是不是極其相似呢?
假如把每一行都看成一個堆,而每個堆中的石子數爲兩個棋子距離差-1,所以我們只需要對每一堆的“石子數”進行異或計算即可。
#include <bits/stdc++.h>
using namespace std;
int n,m,a,b,x,sum;
int main()
{
ios::sync_with_stdio(false);
while(cin>>n>>m)
{
sum=0;
for(int i=1;i<=n;i++)
{
cin>>a>>b;
x=abs(a-b)-1;
sum=sum^x;
}
if(sum)printf("I WIN!\n");
else printf("BAD LUCK!\n");
}
return 0;
}
hdu 1850 Being a Good Boy in Spring Festival
求尼姆博弈先手必勝的方法數。
只要選擇的一堆石子數大於其他所有石子堆異或值,則先手必勝,因爲可以把選擇的那堆石子數減少到與其他所有石子堆異或值相等,從而使所有石子堆異或值爲0,使後手開始選擇時面臨的是必敗狀態。
#include <bits/stdc++.h>
using namespace std;
int n,sum,ans,a[110];
int main()
{
ios::sync_with_stdio(false);
while(cin>>n&&n)
{
sum=0;ans=0;
for(int i=1;i<=n;i++)
{
cin>>a[i];
sum=sum^a[i];
}
for(int i=1;i<=n;i++)
if((sum^a[i])<a[i])ans++;
printf("%d\n",ans);
}
return 0;
}
hdu 1907 John
反尼姆博弈。
尼姆博弈是最先取光石子的人贏,而反尼姆博弈是最先取光石子的人輸。
做法:將所有的石子堆異或,設爲sum,反尼姆博弈中先手必勝有兩種可能:
①sum!=0且存在a[i]>1
②sum=0且所有a[i]=1(因爲每個石子堆只有一個石子且可以證明石子堆個數必爲偶數,所以顯然總是後手最先取光石子,也就是後手必敗,先手必勝)
#include <bits/stdc++.h>
using namespace std;
int t,n,x,sum,cnt;
int main()
{
ios::sync_with_stdio(false);
cin>>t;
while(t--)
{
cin>>n;
sum=0;cnt=0;
for(int i=1;i<=n;i++)
{
cin>>x;
sum=sum^x;
if(x==1)cnt++;//統計只有1個石子的石子堆個數
}
if((sum!=0&&cnt<n)||(sum==0&&cnt==n))printf("John\n");
else printf("Brother\n");
}
return 0;
}
hdu 2147 kiki’s game
打表NP態,找規律。
從右上角向左下角打表NP態,N態(設爲0)表示先手必勝,P態(設爲1)表示後手必勝,初始位置a[1][m]=1。
遞推到某個位置時,如果它的右邊、上邊、右上邊均爲N態,則它爲P態;如果它的右邊、上邊、右上邊中只要有一個是P態,則它爲N態(先手可以把對手的必勝態P態轉化爲自己的必勝態N態)。
打表代碼(不能AC):
#include <bits/stdc++.h>
using namespace std;
const int N=2010;
int n,m,a[N][N];
int main()
{
ios::sync_with_stdio(false);
while(cin>>n>>m&&(n||m))
{
memset(a,0,sizeof(a));
a[1][m]=1;
for(int i=1;i<=n;i++)
for(int j=m;j>=1;j--)
if(a[i][j+1]==0&&a[i-1][j]==0&&a[i-1][j+1]==0)a[i][j]=1;
if(a[n][1]==0)printf("Wonderful!\n");//先手勝
else printf("What a pity!\n");//後手勝
}
return 0;
}
不能AC的原因是超內存(Memory Limit Exceeded),所以要打表前幾項找規律。
顯然可以看出後手勝的概率較低,那麼就找後手勝的規律。
1 1 後手勝
1 2 先手勝
2 1 先手勝
2 2 先手勝
3 1 後手勝
1 3 後手勝
3 2 先手勝
2 3 先手勝
3 3 後手勝
4 1 先手勝
1 4 先手勝
4 2 先手勝
2 4 先手勝
4 3 先手勝
3 4 先手勝
4 4 先手勝
規律:當n和m均爲奇數時,後手勝,否則先手勝。
AC代碼:
#include <bits/stdc++.h>
using namespace std;
int n,m;
int main()
{
ios::sync_with_stdio(false);
while(cin>>n>>m&&(n||m))
{
if((n&1)&&(m&1))printf("What a pity!\n");
else printf("Wonderful!\n");
}
return 0;
}
hdu 1848 Fibonacci again and again
裸的打表SG函數,求sg函數的模板題。
將輸入的三個值對應的sg函數進行異或,異或值爲0則後手勝,否則先手勝。
#include <bits/stdc++.h>
using namespace std;
const int N=1010;
bool vis[N];
int a,b,c,f[N],sg[N];//f[]表示每次能取的數
void get_sg()//打表sg函數
{
memset(sg,0,sizeof(sg));//sg[0]=0
for(int i=1;i<=N;i++)//從1開始遍歷,得到sg[i]對應的值
{
memset(vis,0,sizeof(vis));
for(int j=1;j<=15&&f[j]<=i;j++)//遍歷能取的數f[j]
vis[sg[i-f[j]]]=1;//標記i取了f[j]後對應的sg值
for(int j=0;j<=N;j++)
if(vis[j]==0){sg[i]=j;break;}//第一個未標記的非負數即爲sg[i]
}
}
int main()
{
ios::sync_with_stdio(false);
f[1]=1;f[2]=2;
for(int i=3;i<=15;i++)//f[15]=987<1000,f[16]=1597>1000
f[i]=f[i-1]+f[i-2];
get_sg();
while(cin>>a>>b>>c&&(a||b||c))
{
if(sg[a]^sg[b]^sg[c])printf("Fibo\n");
else printf("Nacci\n");
}
return 0;
}
hdu 1536 S-Nim
這題和上題差不多,還是直接打表sg函數。 記得排序一下 f 數組(f[i]表示每次能取的數)。
還有一個比較玄學的細節:vis標記數組寫成了int類型會導致TLE,改成bool類型就AC了。
#include <bits/stdc++.h>
using namespace std;
const int N=1e4+10;
bool vis[N];//這裏的vis一定要定義成bool類型,定義成int類型會超時!
int n,m,k,x,sum,f[N],sg[N];
void get_sg()
{
memset(sg,0,sizeof(sg));
for(int i=1;i<=N;i++)
{
memset(vis,0,sizeof(vis));
for(int j=1;j<=n&&f[j]<=i;j++)//默認f[]已經升序排列
vis[sg[i-f[j]]]=1;
for(int j=0;j<=N;j++)
if(vis[j]==0){sg[i]=j;break;}
}
}
int main()
{
ios::sync_with_stdio(false);
while(cin>>n&&n)
{
for(int i=1;i<=n;i++)
cin>>f[i];
sort(f+1,f+n+1);//一定要保證f[]升序
get_sg();
cin>>m;
while(m--)
{
cin>>k;
sum=0;
for(int i=1;i<=k;i++)
{
cin>>x;
sum=sum^sg[x];
}
if(sum==0)printf("L");
else printf("W");
}
printf("\n");
}
return 0;
}
hdu 3980 Paint Chain
題意:n元環,每次取m個連續的石子,最後取不了的判負。
n元環可以經過去一次之後變成n-m元鏈,而鏈可以用Nim和來計算sg值。
可以這樣理解: 對於n-m元鏈,每次選擇一個位置取m個石子,從而把剩餘部分分成了兩堆石子(石子數可以爲0),將這兩堆石子的sg值異或,即爲取m個石子之前的sg值。
不過需要注意的一點是取了m個之後變成n-m元鏈,要轉換先後手。
#include <bits/stdc++.h>
using namespace std;
const int N=1010;
bool vis[N];
int t,n,m,sg[N];
void get_sg()//n>m時,打表sg函數
{
memset(sg,0,sizeof(sg));
for(int i=1;i<=n-m;i++)
{
memset(vis,0,sizeof(vis));
for(int j=0;i-m-j>=0;j++)
vis[sg[j]^sg[i-m-j]]=1;//左段長j,右段長i-m-j,總共長i-m
for(int j=0;j<=n-m;j++)
if(vis[j]==0){sg[i]=j;break;}
}
}
int main()
{
ios::sync_with_stdio(false);
cin>>t;
for(int cas=1;cas<=t;cas++)
{
cin>>n>>m;
printf("Case #%d: ",cas);
if(n==m)printf("aekdycoin\n");//先手勝
else if(n<m)printf("abcdxyzk\n");//後手勝
else//n>m時,打表sg函數
{
get_sg();
if(sg[n-m])printf("abcdxyzk\n");//後手勝(轉換先後手)
else printf("aekdycoin\n");//先手勝
}
}
return 0;
}
hdu 5795 A Simple Nim
輸入的s[i]最大爲1e9,sg數組開不了這麼大,所以要打表sg函數找規律。
打表代碼:
#include <bits/stdc++.h>
using namespace std;
int sg[110];
bool vis[110];
void get_sg()
{
memset(sg,0,sizeof(sg));
for(int i=1;i<=100;i++)
{
memset(vis,0,sizeof(vis));
for(int j=0;j<i;j++)//若選擇取石子,後繼節點的sg值爲sg[0]~sg[i-1]
vis[sg[j]]=1;
for(int j=1;j<i;j++)//若選擇把石子分成三堆,第一堆個數j,第二堆個數k,後繼節點的sg值爲sg[j]^sg[k]^sg[i-k-j]
for(int k=1;k<i&&i-k-j>=1;k++)//注意是i-k-j>=1,不是>=0!
vis[sg[j]^sg[k]^sg[i-k-j]]=1;
for(int j=0;j<=100;j++)
if(vis[j]==0){sg[i]=j;break;}
}
}
int main()
{
get_sg();
for(int i=0;i<=100;i++)
printf("%d %d\n",i,sg[i]);
return 0;
}
AC代碼:
#include <bits/stdc++.h>
using namespace std;
int t,n,x,sum;
int main()
{
ios::sync_with_stdio(false);
cin>>t;
while(t--)
{
cin>>n;sum=0;
for(int i=1;i<=n;i++)
{
cin>>x;
if(x%8==0)x--;
else if(x%8==7)x++;//寫else if,不要寫if!
sum^=x;
}
if(sum)printf("First player wins.\n");
else printf("Second player wins.\n");
}
return 0;
}
hdu 2873 Bomb Game
二維sg函數打表。
題意:給定n*m的棋盤,棋盤中有炸彈,每進行一次操作炸彈炸一次,炸一次生成兩個炸彈,分別位於左方和上方(左或上是邊界則不生成),炸完之後原炸彈消失,兩人輪流操作,最後不能引爆的輸。
思路:
一維的情況,等價於多堆取石子的遊戲,sg值即石子數,本題中也就是到1,1的距離。
二維時,引爆每個炸彈後會產生兩個新的炸彈,而這個炸彈的sg即可看做新產生的兩個炸彈的sg的異或(即"NIM和"),這樣只要修改一下一維求sg的函數,變可以構造出二維的sg函數表,對應有炸彈的位置的sg值異或起來就可以判定勝負。
#include <bits/stdc++.h>
using namespace std;
char a[60][60];
bool vis[1010];//vis開大一點
int n,m,sum,sg[60][60];
int get_sg(int x,int y)
{
memset(vis,0,sizeof(vis));
for(int i=1;i<x;i++)
for(int j=1;j<y;j++)
vis[sg[x][j]^sg[i][y]]=1;
for(int i=0;;i++)
if(!vis[i])return i;
}
int main()
{
ios::sync_with_stdio(false);
for(int i=1;i<=50;i++)
for(int j=1;j<=50;j++)
{
if(i==1)sg[i][j]=j-1;
else if(j==1)sg[i][j]=i-1;
else sg[i][j]=get_sg(i,j);//打表
}
/*for(int i=1;i<=50;i++)
for(int j=1;j<=50;j++)
j==50?printf("%3d\n",sg[i][j]):printf("%3d ",sg[i][j]);*/
while(cin>>n>>m&&(n||m))
{
sum=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
cin>>a[i][j];
if(a[i][j]=='#')sum^=sg[i][j];
}
if(sum)printf("John\n");
else printf("Jack\n");
}
return 0;
}