博弈论(SG 函数)学习笔记
本文由 Pretharp 编写,转载需注明出处,禁止用于任何形式的商业用途。
1 引入
1.1 算法相关
本文主要介绍 SG 状态,也会介绍相关前置知识。
2 前置知识
2.1 NP 状态介绍
NP 状态,四舍五入为当前必胜态和当前必败态。引入情境:
桌子上有一包饼干,Pretharp 和 Mijacs 轮流选择吃掉还是不吃掉,吃掉饼干的获得胜利。Pretharp 先手,当双方都采用最优状态时,问输赢。
很显然,一开始这个问题的状态就是必胜态,因为 Pretharp 只要吃掉饼干就赢了,所以一开始状态就是 N 状态。那么对于 Mijacs 来说呢?很显然是一个必败态,也就是 P 状态,因为 Pretharp 不是傻子。
这样手动推理出 NP 状态并不难,可是如果我们将问题拓展:
桌子上有 \(n\) 包饼干,Pretharp 和 Mijacs 轮流选择吃掉一包还是两包,如果当前没得饼干吃了则输掉游戏。Pretharp 先手,当双方都采用最优状态时,问输赢。
先考虑没有饼干的状态,这很显然是一个必败态。那么如果当前还剩下一包或者两包饼干,这则是一个必胜态。证明都很好证明,那么我们怎么用组合游戏的语言去描述当前状态是必胜态还是必败态呢?因为对于还剩下一包饼干或者两包饼干时,当前操作者都可以使用一次操作将操作变为必败态,这个必败态是对于对手而言。也就是说,你可以通过一次操作使得对手必败,那么你就必胜。
那么当前有三包饼干时,Pretharp 必败,为什么?因为 Pretharp 可以将当前状态转换为还剩下一包或者两包饼干的状态,而这两个状态都是必胜态,且这个必胜态是对于对手而言,所以,如果你无论如何操作都会使得对手必胜,你就必败。
根据上面的例子我们可以得到 NP 状态的推导方法:
- 如果当前状态可以通过一次操作变为 P 状态,那么当前状态为 N 状态。
- 如果当前状态通过一次操作只能变为 N 状态,那么当前状态为 P 状态。
2.2 异或和世界观(Nim 游戏&台阶型 Nim 游戏)
Nim 游戏:
先来看一个很经典的问题——Nim 游戏:
桌子上有 \(n\) 堆饼干,第 \(i\) 堆饼干有 \(a_i\) 包饼干。Pretharp 和 Mijacs 可以轮流从其中某一堆饼干中吃掉任意数目的饼干(但是不能不吃),如果没有饼干吃了就算输掉游戏。Pretharp 先手,当双方都采用最优状态时,问输赢。
我们根据定义来推导:
-
我们必然可以将当前状态变成一个异或和为 \(0\) 的情况(也就是必败态)。为什么呢?假设当前异或和为 \(s\),我们只要让某一个数减去 \(a_i - (a_i \oplus s)\) 就可以使得 \(s=0\)。因为异或和不为 \(0\),也就是在二进制下某一位上,所有数在这一位上为 \(1\) 的数的个数是奇数,所以 \(a_i \oplus s < s\) 的数必然存在,所以这一个操作必然合法。所以如果当前异或和不为 \(0\),你一定可以将当前状态变为异或和为 \(0\) 的状态。
-
因为当前状态是必败态,所以当前异或和为 \(0\)。之后你无论怎么操作之后异或和都不可能继续是 \(0\)。所以如果当前状态异或和为 \(0\),你只能将当前状态转换为异或和不为 \(0\) 的状态。
根据上述推导,最终的必败态(所有饼干都被吃完了)的异或和为 \(0\),而若当前态不为 \(0\) 你可以一直把异或和为 \(0\) 的状态留给对手,且对手只能把异或和不为 \(0\) 的状态留给你,直到某一次留给对手的状态为最终态,我们获胜。所以我们得出结论:
- 若 \(\bigoplus\limits_{i=1}^{n}a_i \not= 0\)(即 \(a_i\) 的异或和),则先手必胜(也就是初始状态是必胜态)。
- 相反,先手必败(即初始状态为必败态)。
台阶型 Nim 游戏:
在进入下一个板块之前,我们还需要搞明白一个东西。引入问题:
在一个 \(n+1\) 级的台阶上(级数为 \(0,1,\ldots,n\)),第 \(i\)(\(i \in [1,n]\))级台阶有 \(a_i\) 包饼干放在上面。Pretharp 和 Mijacs 可以轮流选择一个在 \(1\) 到 \(n\) 的台阶(设其为第 \(x\) 级),将上面任意数量的饼干移动到下一级台阶上(即 \(x-1\))。如果饼干到达了第 \(0\) 级台阶上就会被吃掉,无法操作者输掉游戏。Pretharp 先手,当双方都采用最优状态时,问输赢。
先假设 \(n=2\)。不难发现,第 \(1\) 级台阶上的饼干被移动了就相当于是 Nim 游戏中的饼干被吃掉了。可是如果将第 \(2\) 级台阶上的饼干移动就会使第 \(1\) 级台阶上的饼干数目增加,这是 Nim 游戏里没有的操作。但是聪明的你一定可以发现,对手在第 \(2\) 级台阶上移动了多少饼干下来,你就再在第 \(1\) 级台阶上移动多少饼干下去,让这次操作相当于没有操作,并且第 \(2\) 级台阶上的饼干数目有限,所以最终一定会被移动完。此时,答案就很显然了。
我们在此基础上拓展一下,对于 \(2n+1\) 和 \(2n+2\) 我们可以把它们看成一个整体:
- 奇数台阶上的饼干数目就相当于 Nim 游戏中的一堆饼干。
- 偶数台阶上就是一个饼干缓存区(也就是上一自然段所讲述的东西,到达了偶数台阶上的饼干只会临时在奇数饼干上路过一下,并不会停留在上面),即可以把到达了偶数台阶上的饼干看成到达了第 \(0\) 级台阶的饼干。
有了 Nim 游戏的铺垫,不难得出该游戏的结论:若 \(\bigoplus\limits_{i\in[1,n],2\nmid i} a_i \not= 0\)(即所有奇数级台阶上的饼干数目的异或和),则先手必胜,反之亦然。
但重要的不是这个,而是我们要搞清楚:出现了不符合 Nim 游戏的操作,如果可以抵消且这种操作是有节制的,那我们可以无视这种操作。 (注:这一句话是笔者自己总结的,不保证 \(100\%\) 严谨,请不要当作结论使用)。
2.3 \(\operatorname{mex}\) 运算
进入下一个板块前的最后东西。
\(\operatorname{mex}\) 运算,即 minimum exclusion,定义:\(\operatorname{mex(S)=\min\{x|x \in \mathbb{N},x\notin S\}}\)(即一个集合中最小的没出现过的非负整数),例如 \(\operatorname{mex(\{0,1,3\})=2}\)。
3 算法讲解(\(SG\) 函数)
3.1 有向图游戏
引入问题:
有 \(n\) 个有向无环连通图,每个有向图有且仅有一个起点(起点可以到达其所在图内任何一个点,且其所在图内任何点都无法到达)。现在,这 \(n\) 个图上,每个图的起点都放置了一包饼干,Pretharp 和 Mijacs 轮流选择一包饼干沿有向边移动,无法操作者输掉游戏,当双方都采用最优状态时,问输赢。
先定义什么是 \(SG\) 函数:\(\operatorname{SG}(x)=\operatorname{mex}(\{\operatorname{SG}(y_1),\operatorname{SG}(y_2),\ldots,\operatorname{SG}(y_k)\})\),其中 \(x\) 的有向边分别连向 \(y_1,y_2,\ldots,y_k\)。为了方便表述,下文所说的「\(x\) 的 \(SG\) 值」等价于「\(\operatorname{SG}(x)\)」。
我们会发现一些很巧妙地性质:
- 对于一个结点,它可以走到任何一个 \(SG\) 值更小的结点。
- 当一个结点不能再走下去的时候,其 \(SG\) 值为 \(0\)。
- 如果一个结点走一步之后,其 \(SG\) 值变大,一定可以再走一步使其 \(SG\) 值变回原来的值。
我们换个说法,对于结点 \(x\):
- 它有 \(\operatorname{SG\{x\}}\) 个饼干,并且你可以一次拿掉任意数量的饼干。
- 当你不能再在这个结点拿饼干的时候,其饼干数目为 \(0\)。
- 如果对手把饼干数目变多了,你可以把饼干数目变回来。
如果我们在一个图中,把每个结点看成一个状态,起点是初始状态……那一整个有向图就可以抽象为 Nim 游戏中的一堆石子。如果这 \(n\) 个有向图的起点分别是 \(u_1,u_2,\ldots,u_n\),如果 \(\bigoplus\limits_{i=1}^n\operatorname{SG}(u_i) \not= 0\)(即每个有向图的起点的 \(SG\) 值的异或和)先手必胜,反之亦然。
在后续的大部分问题中,我们依然可以把每个子问题看作一个图,每一个图中的结点就是这个子问题所能触及的状态(就像流程图一样)。这只是 \(SG\) 函数最基础的运用,请读者务必将此模型完全理解。
4 例题
4.1 例题 1~2
题目大意:
给定一个有 \(n\) 个点且可能不连通的无向图,每个顶点初始度数为 \(2\)。Alice 和 Bob 轮流在图中选择一个大小在 \([l,r]\) 间的联通子图,并删除这个子图及所有相邻的边。Alice 先手,无法操作者输掉游戏,当双方都采用最优状态时,问输赢。
分析:
很容易发现,图中一开始只有环,在游戏过程中会出现环和链。并且这些环和链之间两两没有联系,且其 \(SG\) 值只与大小有关。我们不妨将每个环和链看成 Nim 游戏里的一堆饼干,并将其 \(SG\) 值算出来。先考虑链,设 \(\operatorname{f}(n)\) 表示一个大小为 \(n\) 的链的 \(SG\) 值。那么有:
解释一下:
- 如果 \(n<l\),很显然已经无法继续分解,故 \(SG\) 值为 \(0\)。
- 如果 \(n \ge l\),那么我们就枚举 \(len\),即在这条链当中删掉长度为 \(len\) 的一部分,再枚举 \(m\),表示删掉一部分后留下了长度分别为 \(m\) 和 \(n-len-m\) 的条链,此时 \(SG\) 值就是两者的 \(SG\) 值异或的结果。
接下来考虑环。设 \(g(n)\) 表示一个有 \(n\) 个结点的环的 \(SG\) 值。有:
即:
- 如果 \(n<l\),显然易得 \(SG\) 值为 \(0\)。
- 如果 \(n \ge l\),即一个有 \(n\) 个结点的链在截取了长度为 \(len\) 的子部分后剩下了一条长度为 \(n-len\) 的链。
若初始的 \(k\) 个环大小分别为 \(siz_i\),那最终若 \(\bigoplus\limits_{i=1}^k g(siz_i) \not= 0\),先手必胜,反之亦然。
此时,我们可以写出一份答案正确,但是时间复杂度错误的代码。
参考代码 1:
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, l, r, p[N], siz[N], f[N], g[N], ans;
bool head[N];
vector<int> tmp;
int mex(vector<int> &sq) {
sort(sq.begin(), sq.end());
sq.erase(unique(sq.begin(), sq.end()), sq.end());
int now = 0;
for(int i = 0; i < sq.size(); i ++) {
if(sq[i] != now) {
return now;
}
now ++;
}
return now;
}
int find(int x) {
return (p[x] == x ? x : p[x] = find(p[x]));
}
signed main() {
cin >> n >> l >> r;
for(int i = 1; i <= n; i ++) {
p[i] = i, head[i] = siz[i] = 1;
}
for(int i = 1, x, y, fx, fy; i <= n; i ++) {
cin >> x >> y;
fx = find(x), fy = find(y);
if(fx == fy) {
continue;
}
p[fx] = fy, head[fx] = 0, siz[fy] += siz[fx];
}
for(int i = l; i <= n; i ++) {
tmp.clear();
for(int len = l; len <= min(r, i); len ++) {
for(int j = 0; j <= i - len; j ++) {
tmp.push_back(f[j] ^ f[i - len - j]);
}
}
f[i] = mex(tmp);
}
for(int i = l; i <= n; i ++) {
tmp.clear();
for(int len = l; len <= min(r, i); len ++) {
tmp.push_back(f[i - len]);
}
g[i] = mex(tmp);
}
for(int i = 1; i <= n; i ++) {
if(head[i]) {
ans ^= g[siz[i]];
}
}
return cout << (ans ? "Alice" : "Bob") << endl, 0;
}
很多时候实现 \(SG\) 函数的时间复杂度都不够优秀,但是我们可以运用人类的智慧。根据打表可以得出结论:
这题本身就是结论题,至于证明参见官方题解。
参考代码 2:
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, l, r, p[N], siz[N], g[N], ans;
bool head[N];
int find(int x) {
return (p[x] == x ? x : p[x] = find(p[x]));
}
signed main() {
cin >> n >> l >> r;
for(int i = 1; i <= n; i ++) {
p[i] = i, head[i] = siz[i] = 1;
}
for(int i = 1, x, y, fx, fy; i <= n; i ++) {
cin >> x >> y;
fx = find(x), fy = find(y);
if(fx == fy) {
continue;
}
p[fx] = fy, head[fx] = 0, siz[fy] += siz[fx];
}
for(int i = 0; i < min(n + 1, l + r); i ++) {
g[i] = i / l;
}
for(int i = 1; i <= n; i ++) {
if(head[i]) {
ans ^= g[siz[i]];
}
}
return cout << (ans ? "Alice" : "Bob") << endl, 0;
}
例题 2:[HNOI2007] 分裂游戏
题目大意:
有编号 \(1\) 到 \(n\) 的 \(n\) 个盒子,初始时第 \(i\) 个盒子内有 \(a_i\) 块饼干,Pretharp 和 Mijacs 轮流选择三个数 \(i,j,k\)(\(1 \le i < j \le k \le n\)),并将盒子 \(i\) 内的饼干数目减一,盒子 \(j,k\) 内的饼干数目加一,无法操作这输掉游戏。当双方都采用最优状态时,问输赢。
分析:
这题的巧妙之处在于你不应是把每个盒子看成子游戏,而是将每块饼干看成子游戏。对于一块饼干,其 \(SG\) 值只会与它所处的盒子编号有关。显然,如果饼干到达了 \(n\) 就无法继续移动,否则其将会分裂为位于 \(j,k\) 的两个子游戏,不难得出位于饼干 \(i\) 的 \(SG\) 值:
至于如何求出有多少第一步可走的方案数及字典序最小的方案,可以枚举第一步走的方式,若走完后状态为必败态则当前这种第一步的走法为合法方案。
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 25;
int sg[N], T, n, a[N], ans, cnt, fi, fj, fk;
vector<int> tmp;
int mex(vector<int> &sq) {
if(!sq.size()) {
return 0;
}
sort(sq.begin(), sq.end());
sq.erase(unique(sq.begin(), sq.end()), sq.end());
for(int i = 0; i < sq.size(); i ++) {
if(sq[i] != i) {
return i;
}
}
return sq.size();
}
void getSG() {
memset(sg, 0, sizeof sg);
for(int i = n; i >= 1; i --) {
tmp.clear();
for(int j = n; j > i; j --) {
for(int k = n; k >= j; k --) {
tmp.push_back(sg[j] ^ sg[k]);
}
}
sg[i] = mex(tmp);
}
}
signed main() {
cin >> T;
while(T --) {
cin >> n, ans = 0;
getSG();
for(int i = 1; i <= n; i ++) {
cin >> a[i];
if(a[i] % 2) {
ans ^= sg[i];
}
}
if(!ans) {
cout << "-1 -1 -1" << endl << 0 << endl;
continue;
}
cnt = fi = fj = fk = 0;
for(int i = 1; i <= n; i ++) {
if(!a[i]) {
continue;
}
for(int j = i + 1; j <= n; j ++) {
for(int k = j; k <= n; k ++) {
if(!((ans ^ sg[i]) ^ sg[j] ^ sg[k])) {
cnt ++;
if(!fi && !fj && !fk) {
fi = i, fj = j, fk = k;
}
}
}
}
}
cout << fi - 1 << " " << fj - 1 << " " << fk - 1 << endl << cnt << endl;
}
return 0;
}

浙公网安备 33010602011771号