SG函数是用于解决博弈论中公平组合游戏(Impartial Combinatorial Games,ICG)问题的一种方法。
一.
ICG:
有两名玩家
两名玩家轮流操作,在一个有限集合内任选一个进行操作,改变游戏当前局面
一个局面的合法操作,只取决于游戏局面本身且固定存在,与玩家次序或者任何其它因素无关
无法操作者,即操作集合为空,输掉游戏,另一方获胜
经典Nim游戏:
地上有n堆石子,每堆石子数量可能不同,两人轮流操作,每人每次可从任意一堆石子里取走任意多枚石子,
可以取完,不能不取,无石子可取者输掉游戏,问是否存在先手必胜的策略。
二.
sg(x):=mex(sg(y)|x->y),mex表示一个集合中未出现的最小自然数,sg(x)=0必败态,sg(x)!=0必胜态
例子,单堆Nim游戏,归纳总结为sg(0)=0,sg(1)=1,sg(2)=2...sg(n)=n,石子数不为0都是必胜态。
同理:对于双堆Nim游戏当n==m时,sg(n,m)=0;其余sg(n,m)!=0
Sprague - Grundy定理:
设x1,x2,x3...为ICG的状态则sg(x1+x2+x3...)=sg(x1)^(异或)sg(x2)^sg(x3)...
利用SG定理,把单一游戏的情况推广到多个子游戏组合的情况
例题(洛谷P2148)[SDOI2009]E& D
思路:先sg打表,把一堆石子拿掉,另一堆任意拆成两堆,等于说由状态(a,b)可以转移到{ (c,d),c + d = a或c + d = b }
对于每一个a,所有c+d=a的(c, d)的SG值集合我们可以先用一个bitset存起来,这样当我们求(a, b)的SG值时我们直接将a和b
对应的两个集合并起来再求mex就好啦。于是得到了打表代码
找到规律后,得到sg公式(a,b)=sg(a/2,b-1/2+1)+1,异或一=每组子状态即可
#include<cstdio>
#include<bitset>
#include<iostream>
using namespace std;
const int N = 10, M = N + 1;//随便调大小
typedef bitset<M> B;
B s[M];
int ans[M][M];
inline int mex(B b) {//mex表示一个集合中未出现的最小自然数,mex模板
int i = 0;
while (b[i])++i;
return i;
}
int main() {
int i, j, k;
for (i = 2; i <= N; ++i)
for (j = 1, k = i - 1; k; ++j, --k)
s[i].set(ans[j][k] = mex(s[j] | s[k]));//枚举合并
for (i = 0; i < N; ++i)printf("%3d", i); puts("");
for (i = 1; i < N; ++i) {//输出矩阵
printf("%2d:", i);
for (j = 1; j+i <= N; ++j)
printf("%3d", ans[i][j]);
puts("");
}
return 0;
}
#include <iostream>
using namespace std;
using ll = long long;
int sg(ll a, ll b){
if (a % 2 && b % 2)
return 0;
else if (a % 2 == 0)
return sg(a / 2, (b - 1) / 2 + 1) + 1;
else
return sg(b, a);
}
int main()
{
ll t, n, x, y;
cin >> t;
while (t--){
int res = 0;
cin >> n;
for (int i = 0; i < n / 2; ++i){
cin >> x >> y;
res ^= sg(x, y);
}
cout << (res ? "YES" : "NO") << endl;
}
return 0;
}