SG函数和SG定理的运用

SG函数和SG定理常用于解决博弈论的相关问题。其中SG函数的求解主要根据MEX运算。什么是MEX运算?MEX( minimal excludant ) 字面上意思是最小除外的那个数。 MEX是对一个集合的运算。指得是对于一个集合 s={a1,a2,an) 集合中未出现的最小非负整数。
举个例子:比如 s={1,2,3} 则MEX(s)=0, 如果s={0,1,2,4},则MEX(s)=3.
SG定理:博弈论中游戏和的SG值等于各个游戏的SG值的Nim和。Nim和也就是我们所说的按位异或运算下面上例题。

1.Fibonacci again and again

题目传送门:HDU1848
详细解释见下面代码。

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5;
#define mst(a) memset(a,0,sizeof s) 
int f[16],sg[N],s[N],m,n,p; //f[i]储存改变状态的所有方式 sg[i]储存每个状态的sg值 ,s[i]保存后继状态所有sg值 
void solve(){
	for(int i=1;i<=N;i++) //遍历 
	{
		mst(s);//每次都要对保存后继状态的集合s清空 
		for(int j=1;j<=16&&f[j]<=i;j++)//对所有满足条件的后继状态的sg值放入到集合s中 
			s[sg[i-f[j]]]=1;
		for(int k=0;;k++)
			if(!s[k])  //进行mex运算找到最小未出现的非负整数 即为sg值 
			{
				sg[i]=k;
				break;
			}	
	} 
}
int main(){
	f[1]=1,f[2]=2;
	for(int i=3;i<=16;i++)//fibnacci递推 
		f[i]=f[i-1]+f[i-2];
	solve();
	while(cin>>m>>n>>p){
		if(!m&&!n&&!p) break;
		puts((sg[m]^sg[n]^sg[p])?"Fibo":"Nacci");//Nim和运算 若非0先手胜,反之后手胜。 
	}
	return 0;
} 

2.巴什博弈的SG解法

如果对于不知道巴什博弈结论的人可以用SG函数直接求解
不过时间复杂度变成了O(nm)数据太大就只能记结论了。

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5;
int sg[N],s[N];
#define mst(a) memset(a,0,sizeof a)
void solve(int n,int m){  //时间复杂度O(nm) 
	for(int i=1;i<=n;i++)
	{
		mst(s);
		for(int j=1;j<=i&&j<=m;j++)
			s[sg[i-j]]=1;
		for(int k=0;;k++)
			if(!s[k])
			{
				sg[i]=k;
				break;
			}
	}
} 
int main(){
	int t,n,m;
	cin>>t;
	while(t--){
		cin>>n>>m;
		solve(n,m);
		puts(sg[n]?"first":"second");
	}
	return 0;
}

3.尼姆博弈的SG解法

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5;
int sg[N],s[N],n,a[N];
void sovle(){
	for(int i=1;i<N;i++)
	{
		memset(s,0,sizeof s);
		for(int j=1;j<=i;j++)
			 s[sg[i-j]]=1;
		for(int k=0;;k++)
			if(!s[k])
			{
				sg[i]=k;
				break;
			}
	}
}
int main(){
	int t;
	cin>>t;
	while(t--){
		cin>>n;
		int ans=0;
		for(int i=1;i<=n;i++)
			cin>>a[i],ans^=a[i];
		puts(ans?"Yes":"No");
	}
	return 0;
}

4.Roy&October之取石子II

题目传送门:P4860
由题目数据我们可知数据很大,显然用SG是不可能的。但是如果我们没找到规律 ,就可以用SG函数进行打表,通过打表发现的规律的可能性更大。下面给个打表代码。

#include<bits/stdc++.h>
using namespace std;
const int N=5e4+5,M=1e5+5;
int p[M],cnt=1,a[N];
void ss(int n){  //欧拉筛 
	for(int i=2;i<=n;i++)
		a[i]=1;
	for(int i=2;i<=n;i++)
	{
		if(a[i]) p[cnt++]=i;
		for(int j=1;j<cnt&&p[j]*i<=n;j++)
		{
			a[i*p[j]]=0;//最小质因数筛合数。 
			if(i%p[j]==0) break;
		}
	 }
}
int sg[N],s[N];
void solve(){ //求sg函数 
	p[0]=1;
	for(int i=1;i<N;i++)
	{
		memset(s,0,sizeof s);
		for(int j=0;p[j]<=i&&j<cnt;j++)
			s[sg[i-p[j]]]=1;
		for(int k=0;;k++)
			if(!s[k])
			{
				sg[i]=k;
				break;
			}
	}
}
int main(){
	ios::sync_with_stdio(false),cin.tie(0);
	ss(N);
	solve();
		for(int i=1;i<=100;i++)
		if(sg[i]) printf("%d:1\n",i);
		else printf("%d:0\n",i);
	return 0; 
} 


从表中我们知道凡是4的倍数都是后手胜。因此我们可以猜想只要n是4的倍数后手胜,否则先手胜。下面上正解代码

事实上,我们自己找规律也能找出来,比如n=1,2,3显然都是先手胜,n=4时,显然先手只能拿1或2或3个,所以n=4为必败态,n=5,6,7时先手都可以通过拿若干个棋子使对手面临n=4这个必败态,所以n=5,6,7为必胜态。
n=8时,先手只能拿1,2,3,5,7 显然后手胜,所以n=8为必败态,依次类推,规律显然。

#include<bits/stdc++.h>
using namespace std;
int main(){
	int t,n;
	cin>>t;
	while(t--){
		cin>>n;
		puts(n%4?"October wins!":"Roy wins!");
	}
	return 0;
}