SG函数解析:
博弈游戏的本质是一个有向图游戏,每个状态(局面)是一个图中一个节点,每个节点可以通向其他多个状态,而每个节点又由n个子游戏组成。
如下图所示,y1、y2、y3…都是一个状态,这些状态是不能同时到达的。而G1、G2、G3…属于y4状态(局面)中的m个子游戏,这m个子游戏是同时存在的,但是这m个子游戏不会影响,即你在G1中的操作不会影响G2的SG值。

区分完了状态(局面)和子游戏之后,我们开始讲解SG函数最关键的mex和异或操作。首先每个局面都有一个SG值,如果该局面SG为0,则当前局面为必败局面。
SG[y1]的值为 mex{SG[y4],SG[y5]},即取不属于{SG[y4],SG[y5]}这个集合的最小非负整数。如 mex{1,2}=0,mex{0,2}=1。对于mex函数可以这样理解,如果y1可达状态中有必败态,即SG值为0的状态,则SG[y1]必定不为0。若y1可达状态中均为必胜态,即SG值均大于0,则SG[y1]必定为0。
mex用来处理后继局面,而异或处理同时存在的子游戏。如局面y4由G1、G2、G3…等子游戏组成,这些子游戏同时存在,因此SG[y4] = SG[G1] xor SG[G2] xor SG[G3]...,此处的证明与NIM游戏证明一致,可以自行查阅。SG问题最重要的就是处理后续局面和某个特定局面中同时存在的多个子游戏的问题,具体的讲解可以参考下面的多个例题。
HDU1848 Fibonacci again and again
题意:一共3堆石子,每人每次任选一堆取走f个,f必须为斐波那契数,最后取光所有石子的人胜。问先手赢还是后手赢。
思路:这是一个比较经典的SG函数问题,我们定义SG[x]表示当一堆石子个数为x时的SG值。因此SG[0]=0,即必败态。此处需注意,SG问题需在初始化时指明必败状态,并且采用记忆化搜索的方式,具体内容见下述代码。
代码:
#include <cstdio>
#include <algorithm>
#define rep(i,a,b) for(int i = a; i <= b; i++)
const int N = 1000+10;
using namespace std;
int f[20],sg[N],n,m,p;
int solve(int x)
{
if(sg[x] != -1) return sg[x];
int vis[2005]; memset(vis,0,sizeof vis);
rep(i,1,15){
if(x < f[i]) break;
vis[solve(x-f[i])] = 1;
}
rep(i,0,2000)
if(!vis[i]) return sg[x] = i;
}
int main()
{
f[1] = 1, f[2] = 2;
rep(i,3,15) f[i] = f[i-1]+f[i-2];
memset(sg,-1,sizeof sg); sg[0] = 0;
while(~scanf("%d%d%d",&n,&m,&p)){
if(n == 0 && m == 0 && p == 0) break;
if((solve(n)^solve(m)^solve(p)) == 0) printf("Nacci\n");
else printf("Fibo\n");
}
return 0;
}
TYVJ2049 魔法珠
题意:有n堆魔法珠,第i堆有ai颗。选择n堆魔法珠中数量大于1的任意一堆。记该堆魔法珠数量为p,p有b1、b2、b3 ... bm这m个小于p的约数。然后将这一堆魔法珠变成m堆,每堆各有b1、b2、b3 ... bm颗魔法珠。最后选择这m堆的任意一堆,令其消失,不可操作者输,问谁能获胜。(1≤n≤100,1≤ai≤1000)
思路:不难发现整个游戏就是由多个魔法珠堆组成的,因此SG[x]表示某一堆魔法珠,个数为x时的SG值。求SG[x]时,若x有b1、b2、b3 ... bm这m个小于x的约数,则可以达到的后继状态一共有m个,即去掉任意一堆达到的状态。而每一个状态中仍有m−1个堆,即m−1个子游戏,因此每一个状态的SG值为这m−1个子游戏SG值的异或和。具体过程见代码。
代码:
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define LOG1(x1,x2) cout << x1 << ": " << x2 << endl;
#define LOG2(x1,x2,y1,y2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << endl;
#define LOG3(x1,x2,y1,y2,z1,z2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << " , " << z1 << ": " << z2 << endl;
typedef long long ll;
typedef double db;
const int N = 1000+10;
const int M = 1e5+100;
const db EPS = 1e-9;
using namespace std;
int sg[N],n;
int solve(int x)
{
if(sg[x] != -1) return sg[x];
int vis[2010]; memset(vis,0,sizeof vis);
int ans[2010];
int tp = 0, ct = 0;
rep(i,1,x-1){
if(x%i == 0){
ans[++ct] = solve(i);
tp ^= ans[ct];
}
}
rep(i,1,ct) vis[tp^ans[i]] = 1;
rep(i,0,2000)
if(vis[i] == 0) return sg[x] = i;
}
int main()
{
memset(sg,-1,sizeof sg);
sg[1] = 0;
while(~scanf("%d",&n)){
int ans = 0;
rep(i,1,n){
int xx; scanf("%d",&xx);
ans ^= solve(xx);
}
if(ans == 0) printf("rainbow\n");
else printf("freda\n");
}
return 0;
}
POJ2311 Cutting Game
题意:给定一张N∗M的矩形网格纸,两名玩家轮流行动。在每一次行动中,可以任选一张矩形网格纸,沿着某一行或某一列的格线,把它剪成两部分。首先剪出1∗1的玩家获胜,求先手是否必胜。(1≤N,M≤200)
思路:此题的SG状态比较好确定,SG[x][y]表示一张x∗y网格纸的SG值。而求解SG[x][y]值时,需要枚举裁剪的行列位置,不同的裁剪方式是一个不同的后继状态,需要对这些后继状态SG值取mex。而每一个后继状态,都包含了两个子游戏,因为有两张纸,而每一张纸都是一个子游戏,因此需要将两个游戏的SG值进行异或。
然后就是确定必败态了,此处需记住,SG函数问题必须在初始化时确定必败态。由于剪出1∗1的玩家获胜,且每个玩家都采取了最优策略,因此一定不会剪出 1∗x 或 x∗1 (x>1) 的情况。所以最后的状态一定是 SG[2][2]=SG[2][3]=SG[3][2]=0,因此在初始化时直接进行赋值即可。
代码:
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define LOG1(x1,x2) cout << x1 << ": " << x2 << endl;
#define LOG2(x1,x2,y1,y2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << endl;
#define LOG3(x1,x2,y1,y2,z1,z2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << " , " << z1 << ": " << z2 << endl;
typedef long long ll;
typedef double db;
const int N = 1000+10;
const int M = 1e5+100;
const db EPS = 1e-9;
using namespace std;
int sg[N][N];
int solve(int n,int m)
{
if(sg[n][m] != -1) return sg[n][m];
int vis[1005]; memset(vis,0,sizeof vis);
rep(i,2,n/2){
int x = solve(i,m);
int y = solve(n-i,m);
vis[x^y] = 1;
}
rep(i,2,m/2){
int x = solve(n,i);
int y = solve(n,m-i);
vis[x^y] = 1;
}
rep(i,0,1005)
if(vis[i] == 0) return sg[n][m] = i;
}
int main()
{
int n,m;
memset(sg,-1,sizeof sg);
sg[2][2] = 0, sg[2][3] = 0, sg[3][2] = 0;
while(~scanf("%d%d",&n,&m))
{
if(solve(n,m) == 0) printf("LOSE\n");
else printf("WIN\n");
}
return 0;
}
POJ3537 Crosses and Crosses
题意:一个长度为n的一维格子,两个人交替行动。每次操作选择一个空的格子填上X,若操作完后,一行中出现了三个连续的X,则获胜。问先手是否能赢。(3≤n≤2000)
思路:不难发现这个游戏的子问题比较好划分,因此我们可以考虑SG函数的做法。SG[x]表示一个长度为x的一维格子的SG值。
求取SG[x]时,我们枚举放X的位置,这里需要注意放完X之后,左右的2个格子也不能再放棋子了,因此假如放的位置是pos,则一维格子被分为两部分,一部分是 max(pos−3,0),另一部分是 max(x−pos−2,0),和0取max的原因是子游戏一定不能为复制。
然后再定义一下必败态,即SG[0]=0,其余代码即与之前的SG问题没有什么差别。
代码:
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define LOG1(x1,x2) cout << x1 << ": " << x2 << endl;
#define LOG2(x1,x2,y1,y2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << endl;
#define LOG3(x1,x2,y1,y2,z1,z2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << " , " << z1 << ": " << z2 << endl;
typedef long long ll;
typedef double db;
const int N = 2000+100;
const int M = 1e5+100;
const db EPS = 1e-9;
using namespace std;
int sg[N],n;
int solve(int x)
{
if(sg[x] != -1) return sg[x];
int vis[2000+5]; memset(vis,0,sizeof vis);
rep(i,1,x){
int xx = solve(max(0,i-3));
int yy = solve(max(0,x-i-2));
vis[xx^yy] = 1;
}
rep(i,0,2005)
if(vis[i] == 0) return sg[x] = i;
}
int main()
{
memset(sg,-1,sizeof sg);
sg[0] = 0;
scanf("%d",&n);
if(solve(n) == 0) printf("2\n");
else printf("1\n");
return 0;
}
总结:SG函数其实是一种暴力搜索,其重点在于找到子状态,然后划分不同的后继局面,后继局面的SG值取mex,子游戏的SG值取异或和。还有一点需要注意的是初始化时需要给出最后必败态的SG值,此处需要注意我们只知道必败态的SG值为0,但必胜态的SG值由于与mex和异或有关,因此我们无法预先给出。