浅析SG函数
前置知识
Mex运算
设 \(S\) 表示一个非负整数集合,定义 \(\operatorname{mex}(S)\) 为求出不属于集合 \(S\) 的最小非负整数运算
公平组合游戏
若一个游戏满足:
- 两名玩家交替行动
- 在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关
- 不能行动的玩家判负
则称它是公平组合游戏,Nim博弈属于公平组合游戏,但常见的棋类游戏就不是公平组合游戏,比如围棋,双方只能分别落黑子或白子,胜负判定也比较复杂,不符合条件 \(2\) 和 \(3\)
有向图游戏
给定一个有向无环图,图中有唯一一个起点,在起点上放着一枚棋子
两名玩家交替把棋子沿着有向边移动,每次可以移动一步,无法移动者判负,该游戏被称为有向图游戏,任何一个公平组合游戏都能转化为有向图游戏
SG函数
定义
有向图游戏中对于每个结点 \(x\) ,设从 \(x\) 出发的有 \(k\) 条有向边,分别到达 \(y_1,y_2,\cdots,y_k\) ,定义 \(\operatorname{SG}(x)\) 为:
有向图游戏的和
设 \(G_1,G_2,\cdots,G_m\) 是 \(m\) 个有向图游戏,定义有向图游戏 \(G\) ,它的规则是每次任选一个有向图游戏 \(G_i\) 并在 \(G_i\) 上行动一步,则 \(G\) 为 \(G_1,G_2,\cdots,G_m\) 的和,且有游戏和的 \(\operatorname{SG}\) 函数值等于各子游戏 \(\operatorname{SG}\) 值得异或和:
Nim博弈的SG值
对于Nim博弈的每一堆物品,我们都可以把它看作一个子游戏,用归纳法容易证明含 \(x\) 个物品的子游戏 \(\operatorname{SG}\) 值为 \(x\) ,那么Nim博弈的 \(\operatorname{SG}\) 值即为各子游戏异或和,也就是物品数的异或和
Bash博弈的SG值
一堆物品数为 \(n\) 的物品,一次最多取 \(m\) 个且不能不取,那么对于 \(x\geq m\) , \(\operatorname{SG}\) 值显然为:
对于 \(x<m\) ,易证 \(\operatorname{SG}(x)=x\)
进一步推出:\(\forall x\in \mathbb{N},\operatorname{SG}(x)=x\bmod (m+1)\)
性质
-
有向图游戏某个局面必胜当且仅当该局面对应的 \(\operatorname{SG}\) 函数值大于 \(0\)
-
有向图游戏某个局面必败当且仅当该局面对应的 \(\operatorname{SG}\) 函数值等于 \(0\)
-
对于任何游戏的和,我们可以将其中任一单一子游戏换成数目为它的 \(\operatorname{SG}\) 值的一堆物品,转化为Nim博弈,因为Nim博弈的子游戏的 \(\operatorname{SG}\) 值等于物品数,替换后游戏和的值不变
第三条性质描述的是:在考虑游戏和的时候,每个单一的游戏的具体细节可以被忽略,可以替换成 \(\operatorname{SG}\) 值相等的其他游戏,因为根据游戏和的运算规则,这样的操作不会使游戏和改变
有向图游戏的Nimk和
设 \(G_1,G_2,\cdots,G_m\) 是 \(m\) 个有向图游戏,定义有向图游戏 \(G\) ,它的规则是每次任选不超过 \(k\) 个有向图游戏, 并在选取的每个子游戏上都行动一步,则 \(G\) 为 \(G_1,G_2,\cdots,G_m\) 的Nimk和
根据性质 \(3\) ,我们根据每个子游戏的 \(\operatorname{SG}\) 值把它等效为一堆物品,就可以用 Nimk博弈 的结论判定胜负了,下面的题目就体现了这一点
POJ 2315
本题是Bash博弈和Nimk博弈的结合,先计算所有子游戏的SG函数值,这等价于Nimk博弈中的每堆物品的物品数,再用结论即可
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
const double PI = 3.1415926535;
int n, m, l, r, k, maxx;
int s[30 + 5], sg[30 + 5];
int main()
{
while(scanf("%d%d%d%d", &n, &m, &l, &r) != EOF) {
k = l / (2.0 * PI * r);
maxx = 0;
for(int i = 1; i <= n; i++) {
scanf("%d", &s[i]);
s[i] = ceil((double)s[i] / (2.0 * PI * r));
sg[i] = s[i] % (k + 1);
maxx = max(maxx, sg[i]);
}
int len = log2(maxx) + 1;
bool win = false;
for(int i = 1; i <= len; i++) {
int s = 0;
for(int j = 1; j <= n; j++) {
s += sg[j] % 2;
sg[j] /= 2;
}
if(s % (m + 1)) {
win = true;
break;
}
}
printf("%s\n", win ? "Alice" : "Bob");
}
return 0;
}
Nim博弈的一种变形
我们定义一个游戏:给定 \(n\) 堆物品,第 \(i\) 堆物品有 \(A_i\) 个,两人轮流取,每次可以从任意一堆里取任意多个物品但不能不取,如果某人在一次操作时把某一堆物品的物品数变为了 \(0\) ,那么他输掉了这次游戏
我们把每一堆物品都看作一个子游戏,显然有 \(\operatorname{SG}(1)=0\) ,\(A_1=A_2=\cdots A_n=1\) 为最终的必败态,此时 \(\operatorname{SG}(A_1)=\operatorname{SG}(A_2)=\cdots=\operatorname{SG}(A_n)=0\) ,可以看作一个有向图游戏的和
归纳易证 \(\operatorname{SG}(x)=x-1\) ,那么先手必胜的条件就是:
对于Anti-Nim,我们仿照上述过程,也可以得出 \(\operatorname{SG}(x)=x-1\) ,但当 \(A_1=A_2=\cdots A_n=1\) 时,并不能确定这一定是必败态,因此也不能用上述方法计算,而必须寻找另外的结论,关于Anti-Nim,可以参考 Anti-Nim博弈原理与证明
总而言之,这两种博弈的区别就是:对于前者,当某个子游戏结束时,整个游戏也就结束了;后者要在所有子游戏都结束时,整个游戏才会结束。有向图游戏和的解法只适用于前者
计算
求 \(\operatorname{SG}\) 函数一般有两种方法:记忆化搜索和打表归纳
记忆化搜索
AcWing 235
枚举有向图游戏当前结点的后继结点,根据定义求 \(\operatorname{SG}\) 值,模板题
#include<bits/stdc++.h>
using namespace std;
int n;
int a[100 + 5];
int sg[1000 + 5];
int SG(int x)
{
if(sg[x] != -1)
return sg[x];
if(x == 1) {
sg[x] = 0;
return sg[x];
}
int i;
vector<int> div;
div.push_back(SG(1));
for(i = 2; i * i < x; i++) {
if(x % i == 0) {
div.push_back(SG(x / i));
div.push_back(SG(i));
}
}
if(i * i == x)
div.push_back(SG(i));
bool f[100 + 5];
memset(f, 0, sizeof(f));
for(int i = div.size() - 1; i >= 0; i--) {
int res = 0;
for(int j = div.size() - 1; j >= 0; j--)
if(j != i)
res ^= div[j];
f[res] = true;
}
int t = 0;
while(f[t])
t++;
sg[x] = t;
return sg[x];
}
int main()
{
memset(sg, -1, sizeof(sg));
while(scanf("%d", &n) != EOF) {
int res = 0;
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
res ^= SG(a[i]);
}
printf("%s\n", res ? "freda" : "rainbow");
}
return 0;
}
AcWing 219
由题意知\(\operatorname{SG}(1,1)=0\) ,但是我们不能简单地根据这一条件推出其他 \(\operatorname{SG}\) 值,例如 \(\operatorname{SG}(1,3)=\operatorname{mex}\{\operatorname{SG}(1,1)\oplus\operatorname{SG}(1,2)\}=0\) ,但 \((1,3)\) 应该为必胜态, \(\operatorname{SG}\) 值不为 \(0\) 。由于任意一个子游戏结束就代表整个游戏结束,所以这题用到的模型正是前文提到的Nim博弈的一种变形,而把 \((1,1)\) 当作最终必败态是不可行的,因为不可能有多个子游戏的状态同时为 \((1,1)\) ,只要任意一个子游戏出现 \((1,1)\) 的状态,整个游戏就结束了,所以我们需要进行转化,找到产生 \((1,1)\) 必须经过的状态,将其当作最终的必败态。故设 \(\operatorname{SG}(2,3)=\operatorname{SG}(3,2)=\operatorname{SG}(2,2)=0\) ,如果所有子游戏的状态都为这三个状态之一,那么就达到了最终必败态,可以用之前的结论解决这一问题
#include<bits/stdc++.h>
using namespace std;
int n, m;
int sg[200 + 5][200 + 5];
int SG(int x, int y)
{
if(sg[x][y] != -1)
return sg[x][y];
bool f[1000 + 5];
memset(f, 0, sizeof(f));
for(int i = 2; i <= x / 2; i++)
f[SG(i, y) ^ SG(x - i, y)] = true;
for(int i = 2; i <= y / 2; i++)
f[SG(x, i) ^ SG(x, y - i)] = true;
int t = 0;
while(f[t])
t++;
sg[x][y] = t;
return sg[x][y];
}
int main()
{
memset(sg, -1, sizeof(sg));
sg[2][2] = sg[2][3] = sg[3][2] = 0;
while(scanf("%d%d", &n, &m) != EOF)
printf("%s\n", SG(n, m) ? "WIN" : "LOSE");
return 0;
}
POJ 3537
本题运用的模型和前一题一样,容易发现把一个格子涂黑后左右一共连续 \(5\) 个格子都不能被涂黑,这样每次涂黑一个格子后删除 \(5\) 个格子,两侧剩下的格子构成了两个子游戏。用 \(\operatorname{SG}(x)\) 表示含 \(x\) 个白格子的状态,那么可以把末状态设为 \(\operatorname{SG}(0)=0\) ,多个子游戏状态为 \(0\) 的情况是存在的,例如初始给定 \(5\) 个格子的情况下涂正中间的那个格子,那么两边的状态就都为 \(0\) ,所以不需要做类似上题的转化。所有子游戏状态都为 \(0\) 代表最终必败态,此时任意涂黑一个格子都会失败
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAX_N = 2000 + 5;
int sg[MAX_N];
int SG(int x)
{
if(sg[x] != -1)
return sg[x];
bool f[105];
memset(f, 0, sizeof(f));
for(int i = 1; i <= x; i++) {
int l = max(i - 3, 0), r = max(x - i - 2, 0);
f[SG(l) ^ SG(r)] = true;
}
int t = 0;
while(f[t])
t++;
sg[x] = t;
return t;
}
int main()
{
int n;
memset(sg, -1, sizeof(sg));
sg[0] = 0;
while(scanf("%d", &n) != EOF)
printf("%d\n", SG(n) ? 1 : 2);
return 0;
}
打表归纳
当问题规模很大时,记忆化搜索可能导致TLE,但我们仍可以通过记忆化搜索的方式打表并归纳出规律从而减小复杂度
HDU 5795
由于HDU目前无法访问,所以给出题目:给定 \(n\) 堆物品,每次可以取其中一堆的任意个但不能不取,或者将其中一堆物品分成三个非空堆,将物品取完者胜利
打表代码:
#include<bits/stdc++.h>
using namespace std;
int sg[1000 + 5];
int SG(int x)
{
if(sg[x] != -1)
return sg[x];
bool f[100 + 5];
memset(f, 0, sizeof(f));
for(int i = 0; i < x; i++)
f[SG(i)] = true;
for(int i = 1; 3 * i <= x; i++)
for(int j = i; i + 2 * j <= x; j++)
f[SG(i) ^ SG(j) ^ SG(x - i - j)] = true;
int t = 0;
while(f[t])
t++;
sg[x] = t;
return sg[x];
}
int main()
{
memset(sg, -1, sizeof(sg));
sg[0] = 0;
for(int i = 1; i <= 25; i++)
printf("%d\n", SG(i));
return 0;
}
观察出结论:
所以可以写出下面的代码,时间复杂度为 \(O(T\times N)\)
#include<bits/stdc++.h>
using namespace std;
int main()
{
int t, n, a, res;
scanf("%d", &t);
while(t--) {
res = 0;
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%d", &a);
if(a % 8 == 0)
a--;
else if(a % 8 == 7)
a++;
res = res ^ a;
}
printf("%s Player wins.\n", res ? "First" : "Second");
}
return 0;
}
Anti-SG
定义
- Anti-SG游戏规定,无法进行下一步的游戏者赢
- Anti-SG其他规则与SG游戏相同
- 对于任意一个Anti-SG游戏,当局面中所有子游戏的 \(\operatorname{SG}\) 值都为 \(0\) 时游戏结束
SJ定理
对于一个Anti-SG游戏先手必胜当且仅当:
- 游戏的 \(\operatorname{SG}\) 函数不为 \(0\) 且游戏中某个单一游戏的 \(\operatorname{SG}\) 函数大于 \(1\)
- 游戏的 \(\operatorname{SG}\) 函数 为 \(0\) 且游戏中没有单一游戏的 \(\operatorname{SG}\) 函数大于 \(1\)
证明
参考贾志豪的《组合游戏略述——浅谈SG游戏的若干拓展及变形》2.1
Every-SG
定义
设 \(G_1,G_2,\cdots G_m\) 是 \(m\) 个有向图游戏,定义有向图游戏 \(G\) ,它的规则是每次必须把可以移动的子游戏 \(G_i\) 移动一步,最后无法移动者输
可以概括为:
- 对于没有结束的单一游戏,游戏者必须对该游戏进行进一步决策
- 其他规则与普通的SG游戏相同
分析
题目的要求实际上可以看作:不论之前结束的子游戏输赢与否,只要最后一个子游戏胜利,那么就取得了最终胜利
因此当一个人知道某个子游戏必然失败时他一定会尽可能缩短这个子游戏的时间,当他知道某个子游戏必然胜利时他一定尽可能延长这个子游戏的时间,所以Every-SG游戏与普通游戏的区别就是多了一维时间的博弈:
- 对于 \(\operatorname{SG}\) 值为 \(0\) 的点,要求最少需要多少步结束
- 对于 \(\operatorname{SG}\) 值不为 \(0\) 的点,要求最多需要多少步结束
对于状态 \(v\) ,我们用 \(f(v)\) 表示这个值
结论
可以从最大值的最小值/最小值的最大值这一角度理解 \(f\) 的表达式
先手必胜当且仅当单一游戏中最大的 \(f\) 为奇数
证明
参考贾志豪的《组合游戏略述——浅谈SG游戏的若干拓展及变形》2.3