SG函数解析:

博弈游戏的本质是一个有向图游戏,每个状态(局面)是一个图中一个节点,每个节点可以通向其他多个状态,而每个节点又由nn个子游戏组成。
如下图所示,y1y_1y2y_2y3y_3…都是一个状态,这些状态是不能同时到达的。而G1G_1G2G_2G3G_3…属于y4y_4状态(局面)中的mm个子游戏,这mm个子游戏是同时存在的,但是这mm个子游戏不会影响,即你在G1G_1中的操作不会影响G2G_2SGSG值。

【博弈 —— SG函数详解+例题解析】

区分完了状态(局面)和子游戏之后,我们开始讲解SGSG函数最关键的mexmex和异或操作。首先每个局面都有一个SGSG值,如果该局面SGSG00,则当前局面为必败局面。
SG[y1]SG[y_1]的值为 mex{SG[y4],SG[y5]}mex\left\{ SG[y_4],SG[y_5] \right\},即取不属于{SG[y4],SG[y5]}\left\{ SG[y_4],SG[y_5] \right\}这个集合的最小非负整数。如 mex{1,2}=0,mex{0,2}=1mex\left\{ 1,2\right\}=0,mex\left\{ 0,2\right\}=1。对于mexmex函数可以这样理解,如果y1y_1可达状态中有必败态,即SGSG值为00的状态,则SG[y1]SG[y_1]必定不为00。若y1y_1可达状态中均为必胜态,即SGSG值均大于00,则SG[y1]SG[y_1]必定为00
mexmex用来处理后继局面,而异或处理同时存在的子游戏。如局面y4y_4G1G_1G2G_2G3G_3…等子游戏组成,这些子游戏同时存在,因此SG[y4]SG[y_4] = SG[G1] xor SG[G2] xor SG[G3]...SG[G_1]\ xor\ SG[G_2]\ xor\ SG[G_3]...,此处的证明与NIMNIM游戏证明一致,可以自行查阅。SGSG问题最重要的就是处理后续局面和某个特定局面中同时存在的多个子游戏的问题,具体的讲解可以参考下面的多个例题。

HDU1848 Fibonacci again and again

题意:一共33堆石子,每人每次任选一堆取走ff个,ff必须为斐波那契数,最后取光所有石子的人胜。问先手赢还是后手赢。
思路:这是一个比较经典的SGSG函数问题,我们定义SG[x]SG[x]表示当一堆石子个数为xx时的SGSG值。因此SG[0]=0SG[0] = 0,即必败态。此处需注意,SGSG问题需在初始化时指明必败状态,并且采用记忆化搜索的方式,具体内容见下述代码。
代码
#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; //多种后继状态取mex
	}
	rep(i,0,2000)
		if(!vis[i]) return sg[x] = i; //取mex的过程
}

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 魔法珠

题意:有nn堆魔法珠,第ii堆有aia_i颗。选择nn堆魔法珠中数量大于11的任意一堆。记该堆魔法珠数量为ppppb1b2b3 ... bmb_1、b_2、b_3\ ...\ b_mmm个小于pp的约数。然后将这一堆魔法珠变成mm堆,每堆各有b1b2b3 ... bmb_1、b_2、b_3\ ...\ b_m颗魔法珠。最后选择这mm堆的任意一堆,令其消失,不可操作者输,问谁能获胜。(1n100,1ai1000)(1\leq n\leq 100,1\leq a_i\leq 1000)
链接http://contest-hunter.org:83/contest/0x3B「数学知识」练习/3B16 魔法珠
思路:不难发现整个游戏就是由多个魔法珠堆组成的,因此SG[x]SG[x]表示某一堆魔法珠,个数为xx时的SGSG值。求SG[x]SG[x]时,若xxb1b2b3 ... bmb_1、b_2、b_3\ ...\ b_mmm个小于xx的约数,则可以达到的后继状态一共有mm个,即去掉任意一堆达到的状态。而每一个状态中仍有m1m-1个堆,即m1m-1个子游戏,因此每一个状态的SGSG值为这m1m-1个子游戏SGSG值的异或和。具体过程见代码。
代码
#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

题意:给定一张NMN*M的矩形网格纸,两名玩家轮流行动。在每一次行动中,可以任选一张矩形网格纸,沿着某一行或某一列的格线,把它剪成两部分。首先剪出111*1的玩家获胜,求先手是否必胜。(1N,M200)(1\leq N,M\leq 200)
思路:此题的SGSG状态比较好确定,SG[x][y]SG[x][y]表示一张xyx*y网格纸的SGSG值。而求解SG[x][y]SG[x][y]值时,需要枚举裁剪的行列位置,不同的裁剪方式是一个不同的后继状态,需要对这些后继状态SGSG值取mexmex。而每一个后继状态,都包含了两个子游戏,因为有两张纸,而每一张纸都是一个子游戏,因此需要将两个游戏的SGSG值进行异或。
然后就是确定必败态了,此处需记住,SGSG函数问题必须在初始化时确定必败态。由于剪出111*1的玩家获胜,且每个玩家都采取了最优策略,因此一定不会剪出 1x1*xx1 (x&gt;1)x*1\ (x&gt;1) 的情况。所以最后的状态一定是 SG[2][2]=SG[2][3]=SG[3][2]=0SG[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){ //双方采取最优策略进行行动,一定不会剪出1*x或x*1的情况
		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; //双方都按最优策略进行,不会出现 n=1,m>=3 或者 n>=3,m=1 的情况。
	//因此最后的状态是(2,2)、(2,3)、(3,2)
	while(~scanf("%d%d",&n,&m))
	{
		if(solve(n,m) == 0) printf("LOSE\n");
		else printf("WIN\n");
	}
	return 0;
}

POJ3537 Crosses and Crosses

题意:一个长度为nn的一维格子,两个人交替行动。每次操作选择一个空的格子填上XX,若操作完后,一行中出现了三个连续的XX,则获胜。问先手是否能赢。(3n2000)(3\leq n\leq 2000)
思路:不难发现这个游戏的子问题比较好划分,因此我们可以考虑SGSG函数的做法。SG[x]SG[x]表示一个长度为xx的一维格子的SGSG值。
求取SG[x]SG[x]时,我们枚举放XX的位置,这里需要注意放完XX之后,左右的22个格子也不能再放棋子了,因此假如放的位置是pospos,则一维格子被分为两部分,一部分是 max(pos3,0)max(pos-3,0),另一部分是 max(xpos2,0)max(x-pos-2,0),和00maxmax的原因是子游戏一定不能为复制。
然后再定义一下必败态,即SG[0]=0SG[0] = 0,其余代码即与之前的SGSG问题没有什么差别。
代码
#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;
}

总结SGSG函数其实是一种暴力搜索,其重点在于找到子状态,然后划分不同的后继局面,后继局面的SGSG值取mexmex,子游戏的SGSG值取异或和。还有一点需要注意的是初始化时需要给出最后必败态的SGSG值,此处需要注意我们只知道必败态的SGSG值为00,但必胜态的SGSG值由于与mexmex和异或有关,因此我们无法预先给出。

相关文章:

  • 2021-08-05
  • 2021-05-24
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2021-11-20
猜你喜欢
  • 2021-07-16
  • 2022-12-23
  • 2021-06-03
  • 2021-08-17
  • 2022-12-23
  • 2021-06-17
  • 2021-07-01
相关资源
相似解决方案