算法学习————SG函数和SG定理

其实我自己也不是很明白吧,之前考过几次博弈但我都觉得太难没学SG

SG函数应用的场景

组合游戏

在竞赛中,组合游戏的题目一般有以下特点

  1. 题目描述一般为A,B,2人做游戏

  2. A,B交替进行某种游戏规定的操作,每操作一次,选手可以在有限的操作(操作必须合法)集合中任选一种。

  3. 对于游戏的任何一种可能的局面,合法的操作集合只取决于这个局面本身,不取决于其它因素(跟选手,以前的所有操作无关)

  4. 如果当前选手无法进行合法的操作,则为负

必胜点和必败点的概念

必败点(P点) 前一个(previous player)选手将取胜的点称为必败点

必胜点(N点) 下一个(next player)选手将取胜的点称为必胜点

SG函数

先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3

对于任意状态x,定义SG(x) = mex(S),其中S是x后继状态的SG函数值的集合。如x有三个后继状态a,b,c的SG值分别为SG(a),SG(b),SG(c),那么SG(x)=mex{SG(a),SG(b),SG(c)}。 这样当我没有后继状态的时候集合S的终态必然是空集,所以SG函数的终态为SG(x)=0,当且仅当x为必败点P时。

虽然我也不知道为什么要这么求,但是数学就是这么神奇呀

SG定理

游戏和的SG函数等于各子游戏的SG函数的Nim和。

公式说明:\(SG(x_1,x_2,x_3\dots x_n) = SG(x_1)\bigoplus SG(x_2)\dots\bigoplus SG(x_n)\)

应用

具体怎么应用呢?我可以递归求出一部分情况的SG值,然后瞪眼发现规律

例题:[SDOI2009]E&D

为了好好的总结,我应该是以后多会按照这个套路来写SG了

首先肯定是要先打表了,递归把所有他的后继情况求出来回溯的时候对于所有后继状态取mex

一个小tips:递归的时候数组可能会发生改变所以我这里vis定义在函数内,不过这要视写的代码而定

毕竟考场上没人告诉你对错,对着错误的表推结论,肯定是推不出来的,大家还是要谨慎一些

打表的代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <map>
#define B cout<<"Breakpoint"<<endl;
#define O(x) cout<<#x<<" "<<x<<" "<<endl;
#define o(x) cout<<x<<" "<<x<<" ";
using namespace std;
int read(){
	int x = 1,a = 0;char ch = getchar();
	while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
	while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
	return x*a;
}
const int maxn = 1e5+10; 
int T;
map<pair<int,int>,int> mp;
int getSG1(int a,int b){
	bool vis[maxn];
	if (mp.count(make_pair(a,b))) return mp[make_pair(a,b)];
	memset(vis,0,sizeof(vis));
	for (int i = 1;i < a;i++) vis[getSG1(i,a-i)] = 1;
	for (int i = 1;i < b;i++) vis[getSG1(i,b-i)] = 1;
	for (int i = 0;i <= 10000;i++) if (!vis[i]){mp[make_pair(a,b)] = i;break;}
	return mp[make_pair(a,b)];
}
int a[maxn],ans[15][15];
int main(){
	for (int i = 1;i <= 20;i++){
		for (int j = 1;j <= 20;j++){
			ans[i][j] = getSG1(i,j);
		}
	}
	for (int i = 1;i <= 20;i++){
		for (int j = 1;j <= 20;j++) cout<<ans[i][j]<<" ";
		cout<<endl;
	}
	return 0;
}

然后这个表打出来应该是这个样子的:

我们不难发现以下几点:(因为我注意到网上题解都是说,不难发现规律,但是直接给了结论)

  • 相同的数字连在一起的最小单位是个3个方格的三角形,我们就可以看成是两边(横纵坐标有且仅有一个为奇数)等于中间

  • 0的位置横纵坐标都是奇数

  • 由第一条我们可以想,能否解决中间的块,就能解决两边的块,这个我觉得会稍微难一点

对于中间块(x,y)的SG值,他等于(x/2,y/2)的SG值+1,也就是说每个对角线上2的幂的位置是递增的

因为我会除以2,所以这个复杂度是一个log的,我们可以求出每一组石子的SG值,然后看异或起来是否等于0就能解决此题了

代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <map>
#define B cout<<"Breakpoint"<<endl;
#define O(x) cout<<#x<<" "<<x<<" "<<endl;
#define o(x) cout<<x<<" "<<x<<" ";
using namespace std;
int read(){
	int x = 1,a = 0;char ch = getchar();
	while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
	while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
	return x*a;
}
int getSG(int x,int y){
	if (x&1 && y&1) return 0;
	if (x&1) return getSG(x+1,y);
	if (y&1) return getSG(x,y+1);
	return getSG(x >> 1,y >> 1)+1;
}
int T,n;
int main(){
	T = read();
	while (T--){
		n = read();
		int ans = 0;
		for (int i = 1;i <= (n >> 1);i++){
			int x = read(),y = read();
			ans ^= getSG(x,y);
		}
		if (ans == 0) printf("NO\n");
		else printf("YES\n");
	} 
	return 0;
} 
posted @ 2021-07-08 23:11  小又又yyyy  阅读(474)  评论(0编辑  收藏  举报