一、Bash博弈
【巴什博奕】
题目背景:一共有n颗石子,两个人轮流拿,每次可以拿1~k颗石子,拿到最后一颗石子的人获胜。问:先手是否有必胜策略?
在回答这个问题之前,我们可以从题目背景中总结出来三个性质:
① n = 0时,必败。
② n %(k + 1)≠ 0,说明余数m在1~k之间,也就是说,我们必定能找到一个操作(取走m颗石子)使得剩余石子 a %(k + 1)= 0。
③ n %(k + 1)= 0,那么在取走一定数量的石子之后,剩余石子a只有两种情况,第一种:a %(k + 1)= 0;第二种:a % (k + 1)≠ 0。但是,第一种情况不可能成立!因为一开始的n是k+1的倍数,想要剩余石子a也是k+1的倍数的话,就只能取走k+1的整数倍数量的石子(最少也要取k+1颗石子),这就违背题目规则了,因为规则要求只能取1~k颗石子。也就是说,无论我们怎么操作,剩余石子a %(k + 1)≠ 0。

在得出上面三个性质之后,问题的证明也就很简单了:

结论:
当n %(k + 1)= 0 时,先手必败;
当n %(k + 1)≠ 0 时,先手必胜。
二、Nim博弈
【尼姆博弈】
题目背景:n堆物品,每堆分别有a1,a2,a3...an个,两个玩家轮流取走任意一堆的任意个物品,但不能不取。取走最后一个物品的人获胜。问:先手或者后手是否有必胜策略?
证明:
如果a1^a2^a2^...^an = s ≠ 0,设s的二进制表示中最高位为第k位。那么a1,a2,a3...an一定有奇数个ai的二进制第k位为1。那么随意拿出一个ai,用ai^s去替换ai,那么最终异或结果就是0,因为ai^s < ai,所以一定能够替换,且拿走石子的数量为ai - ai ^ s。

如果a1^a2^a3^...^an = s = 0,那么二进制表示中,同一位的1出现次数一定是偶数。此时执行一次操作之后,必定会使某一位的二进制由1变成0,那么这一位的异或结果一定是1,即新的a1^a2^...^an ≠ 0。

结论:
当a1^a2^a3^...^an = 0,先手必败;
当a1^a2^a3^...^an ≠ 0,先手必胜。
【阶梯型Nim游戏】
题目背景:有1~n级台阶,第 i 级台阶上放了a[i] 个石子,两名玩家轮流将任意级台阶的任意个石子移到下一级台阶上,移到地面上的石子不能继续移动,移走最后一个石子的玩家获胜。问:先手是否存在必胜策略?
性质:当某一位玩家遇到奇数台阶没有石子时,无论偶数台阶的石子如何分布,该玩家必败。
证明:
有了上面的性质,那么问题就变成了只考虑奇数台阶的经典Nim游戏。对于先手而言,面对的是奇数台阶异或结果不为0,那么总有一个操作使得异或结果为0,然后交给后手。对于后手而言,面对的是奇数台阶异或结果为0的情况。如果移动偶数台阶的石子,先手可以跟着模仿,依旧将异或为0的状态交给后手;如果移动奇数台阶的石子(相当于Nim博弈中从某一堆石子中拿走一些石子),先手再次遇到不等于0的情况,就可以再次重复上述步骤。因此,无论后手怎么操作,先手都会把奇数台阶异或为0的状态交给后手。
结论:
如果奇数台阶上的a1^a3^a5^... ≠ 0,则先手必胜;
否则,先手必败。
代码实现:
cpp
#include <iostream>
using namespace std;
int main()
{
int n;
cin >> n;
int ret = 0;
for (int i = 1;i <= n;i++)
{
int x;
cin >> x;
if (i & 1)
ret ^= x;
}
if (ret)
cout << "win" << endl;
else
cout << "lose" << endl;
return 0;
}
三、SG函数
【有向图游戏】
题目背景:给定一个有向无环图,图中只有一个起点,在起点上放上一个棋子,两个玩家轮流沿着有向边移动棋子,每次走一步,不能走的玩家失败。
【mex运算(minimum exclusion)】
mex(S) 表示找出不属于集合S的最小非负整数。
例如:S = {0,1,2,3,10},mex(S) = 4;S = {2,3,4},mex(S) = 0。
【SG函数】
在有向无环图中,当某一个状态 x 有 k 个后继 y1,y2,y3,...,yk,那么有:SG(x) = mex({SG(y1),SG(y2),...,SG(yk)}) 。
特殊的,如果 x 没有后继,SG(x) = 0。

结论:
在有向图游戏中,SG(x) ≠ 0 的状态为必胜态,SG(x) = 0 的状态为必败态。
证明:
根据mex的运算规则,如果SG(x) ≠ 0,说明x这个点的后继结点中,一定存在SG(y) = 0 的结点;如果SG(x) = 0,说明x这个点的后继结点中,全部都是SG(y) ≠ 0 的结点。

【利用SG函数解决Bash博弈】
实际上,大部分的公平组合游戏都可以转换为有向图游戏。
当Bash游戏中的 n 和 k 确定之后,就可以把所有的状态图画出来。因为游戏一定能结束,所以必定是一个有向无环图(见 图-1)。
图-1
那么,就可以用SG函数解决Bash博弈。
【利用SG函数,打表找规律】
对于上述Bash游戏,如果把所有的SG值打印出来,就能通过表格中数的规律,得到Bash游戏的结论(见 图-2)。
图-2
【SG定理】
由n个有向图游戏组成的组合游戏,设起点分别为s1,s2,s3,...,sn。
当SG(s1) ^ SG(s2) ^ SG(s3)^...^SG(sn) ≠ 0 时,先手必胜;反之,先手必败。
也就是说,如果一个游戏是由若干个ICG游戏组成的,那么把每一个ICG游戏的SG值异或在一起,就可以判断出所有游戏下的胜负情况。
【利用SG定理解决Nim游戏】
Nim游戏的每一堆石子,都可以看做是一个ICG游戏。因此Nim游戏本身就是由若干个ICG游戏组成的。
在每一个游戏中,SG值都是石子个数(见 图-3)。
图-3
因此,Nim游戏的结论与SG定理是对应的。
代码实现:
cpp
#include <iostream>
#include <cstring>
#include <vector>
#include <unordered_set>
using namespace std;
const int N = 2010;
int n, m, k;
vector<int> edges[N];
int f[N]; // 存每一个点的SG值
int sg(int u)
{
if (f[u] != -1)
return f[u];
unordered_set<int> mp;
for (auto v : edges[u])
{
mp.insert(sg(v));
}
for (int i = 0;;i++)
{
if (!mp.count(i))
{
return f[u] = i;
}
}
}
int main()
{
cin >> n >> m >> k;
while (m--)
{
int x, y;
cin >> x >> y;
edges[x].push_back(y);
}
memset(f, -1, sizeof f);
int ret = 0;
for (int i = 1;i <= k;i++)
{
int x;
cin >> x;
ret ^= sg(x);
}
if (ret)
cout << "win" << endl;
else
cout << "lose" << endl;
return 0;
}
【利用SG函数,打表找规律】
对于很多ICG游戏,如果使用SG函数解决的话,时间复杂度会很高。
如果直接想结论很困难时,可以把所有的SG值从小到大打印出来,通过找规律,进而猜出游戏的结论。

【打表代码实现】
cpp
#include <iostream>
#include <unordered_set>
using namespace std;
int a[] = {1, 2, 3, 4, 5, 7, 8, 9, 11, 13, 16, 17, 19};
int sg(int x)
{
unordered_set<int> mp;
for(auto y : a)
{
if(x - y < 0)
break;
mp.insert(sg(x - y));
}
for(int i = 0; ; i++)
{
if(!mp.count(i))
return i;
}
}
int main()
{
for(int i = 0; i <= 20; i++)
{
cout << i << " -> " << sg(i) << endl;
}
return 0;
}
【程序运行结果】

可以看到,SG函数的值的变化周期为6。因此,这道题就是n % 6 = 0的时候为必败态。
四、经典例题
【题目描述】

【算法原理】

【代码实现】
cpp
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 15, M = 1010;
int n, m;
int a[N], b[N];
int f[M];
int sg(int x)
{
if (f[x] != -1)
return f[x];
unordered_set<int> mp;
for (int j = 1;j <= m && x - b[j] >= 0;j++)
mp.insert(sg(x - b[j]));
for (int i = 0;;i++)
{
if (!mp.count(i))
{
return f[x] = i;
}
}
}
int main()
{
cin >> n;
for (int i = 1;i <= n;i++)
cin >> a[i];
cin >> m;
for (int i = 1;i <= m;i++)
cin >> b[i];
memset(f, -1, sizeof f);
int s = 0;
for (int i = 1;i <= n;i++)
s ^= sg(a[i]);
if (s == 0)
cout << "NO" << endl;
else
{
cout << "YES" << endl;
for (int i = 1;i <= n;i++)
{
for (int j = 1;j <= m && a[i] - b[j] >= 0;j++)
{
if ((s ^ sg(a[i] - b[j]) ^ sg(a[i])) == 0)
{
cout << i << " " << b[j] << endl;
return 0;
}
}
}
}
return 0;
}
【题目描述】

【算法原理】
这道题并不是公平组合游戏,而是一个反常游戏(胜者为第一个无法行动的玩家),因此不能直接按照有向图游戏来解决。我们可以先找出必败态,然后把必败态当成有向图的终点,那么反常游戏就变成了公平组合游戏。
如果状态为"不得不切出边长为1的长条",那么就是必败态。(比如2×2,2×3,3×2,3×3的矩形)

【代码实现】
cpp
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 210;
int n, m;
int f[N][N];
int sg(int n, int m)
{
if (f[n][m] != -1)
return f[n][m];
unordered_set<int> mp;
for (int i = 2;i <= n - 2;i++)
mp.insert(sg(i, m) ^ sg(n - i, m));
for (int j = 2;j <= m - 2;j++)
mp.insert(sg(n, j) ^ sg(n, m - j));
for (int i = 0;;i++)
{
if (!mp.count(i))
return f[n][m] = f[m][n] = i;
}
}
int main()
{
memset(f, -1, sizeof f);
while (cin >> n >> m)
{
int ret = sg(n, m);
if (ret)
cout << "WIN" << endl;
else
cout << "LOSE" << endl;
}
return 0;
}