(一).NIM博弈
1)游戏规则
通常的Nim游戏的定义是这样的:有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。
2)分析
定义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。
3)结论
根据定义,证明一种判断position的性质的方法的正确性,只需证明三个命题: 1、这个判断将所有terminal position判为P-position;2、根据这个判断被判为N-position的局面一定可以移动到某个P-position;3、根据这个判断被判为P-position的局面无法移动到某个P-position。
第二个命题,对于某个局面(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问题就这样基本上完美的解决了。
(二) SG函数
1)mex运算
首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。
对于一个给定的有向无环图,定义关于图的每个顶点的Sprague-Garundy函数g如下:g(x)=mex{ g(y) | y是x的后继 }。
2)SG函数的性质
首先,所有的terminal position所对应的顶点,也就是没有出边的顶点,其SG值为0,因为它的后继集合是空集。然后对于一个g(x)=0的顶点x,它的所有后继y都满足 g(y)!=0。对于一个g(x)!=0的顶点,必定存在一个后继y满足g(y)=0。
SG(X)=0:代表当前状态为必败状态
(三)练习题:
problem one: HDU 4848 Fibonacci again and again
分析:
nim 博弈的简单变形,只需要预处理出(1,1000)的数的SG函数的值即可。
代码如下:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 1010;
int f[30];
int sg[maxn+1];
int vis[maxn+1];
void init(){
f[0]=1,f[1]=1;
for(int i=2;i<20;i++)
f[i]=f[i-1]+f[i-2];
}
void solve(){
init();
memset(sg,0,sizeof(sg));
for(int i=1;i<=1000;i++){
memset(vis,0,sizeof(vis));
for(int j=1;j<maxn&&f[j]<=i;j++)
vis[sg[i-f[j]]]=1;
for(int j=0;;j++){
if(!vis[j]){
sg[i]=j;
break;
}
}
}
}
int main()
{
int n,m,p;
solve();
while(~scanf("%d%d%d",&n,&m,&p)){
if(n==0&&m==0&&p==0)
break;
if(sg[n]^sg[m]^sg[p])
puts("Fibo");
else
puts("Nacci");
}
return 0;
}
problem two:HDU 1907 John
分析:
nim博弈,注意全部为1的情况。
代码如下:
Code Render Status : Rendered By HDOJ C++ Code Render Version 0.01 Beta
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 50;
int a[maxn];
int main()
{
int t,n;
scanf("%d",&t);
while(t--){
scanf("%d",&n);
int s=0,num=0;
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
s^=a[i];
if(a[i]>1) num++;
}
if((s&&num)||(!s&&!num))
puts("John");
else
puts("Brother");
}
return 0;
}
problem there :HDU 3032 Nim or not Nim?
分析:
nim博弈的变形,每次可以从一堆取若干,或者把一堆变成两堆。打表找SG函数的规律。
代码如下:
打表找规律的代码:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 1000010;
int a[maxn];
int sg[maxn];
int get(int x){
int vis[1000];
memset(vis,0,sizeof(vis));
if(sg[x]!=-1) return sg[x];
for(int i=x-1;i>=0;i--)
vis[get(i)]=1;
for(int i=1;i<=x/2;i++){
int ans = 0;
ans^=get(i);
ans^=get(x-i);
vis[ans]=1;
}
for(int i=0;;i++){
if(!vis[i]){
sg[x]=i;
break;
}
}
return sg[x];
}
int main()
{
memset(sg,-1,sizeof(sg));
sg[0]=0;
int n;
while(~scanf("%d",&n)){
for(int i=0;i<20;i++){
printf("sg( %d ) = %d\n",i,get(i));
}
}
return 0;
}
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int get(int x){
if(x==0) return 0;
if(x%4==0)
return x-1;
if(x%4==3)
return x+1;
return x;
}
int main()
{
int t,n,x;
scanf("%d",&t);
while(t--){
scanf("%d",&n);
int ans = 0;
for(int i=0;i<n;i++){
scanf("%d",&x);
ans^=get(x);
}
if(ans) puts("Alice");
else puts("Bob");
}
return 0;
}