0. 前言
一堆关于基础博弈论算法题的相关定义:
NIM游戏:
给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。
我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。
NIM博弈不存在平局,只有先手必胜和先手必败两种情况。
定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0
公平组合游戏ICG:
若一个游戏满足:
- 由两名玩家交替行动;
- 在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
- 不能行动的玩家判负;
则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。
有向图游戏:
给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。
任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。
Mex运算:
设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即: mex(S) = min{x}, x属于自然数,且x不属于S
SG函数:
在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即: SG(x) = mex({SG(y1), SG(y2), …, SG(yk)}) 特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。
有向图游戏的和:
设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …,Gm的和。 有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即: SG(G) = SG(G1) ^ SG(G2) ^ …^ SG(Gm)
定理:
有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。
1. Nim 游戏+模板题
重点: 式子推导
公式梳理的还是很清楚的,就是字难看…
总结:初始所有石子异或值为 0,则先手必败,异或值不为 0,则先手必胜。
代码:
#include <iostream>
using namespace std;
int main() {
int n, res = 0;
cin >> n;
while (n --) {
int a;
cin >> a;
res ^= a;
}
if (res) puts("Yes");
else puts("No");
return 0;
}
2. 台阶 - Nim 游戏+变种题
重点: 式子推导
某些式子需要用到 Nim
游戏的些许证明,翻上去看就行了。字迹潦草…
总结:初始所有奇数阶石子异或值为 0,则先手必败,异或值不为 0,则先手必胜。
思考:为什么不判断偶数级台阶的异或呢?
- 理解方式一:必败态是所有奇数阶台阶的异或和为 0,其余的为必胜态。因为每一个这种定义的必败态不论怎么操作,对手都可以再把你转移回必败态;反之任何一个必胜态,一定存在一种转移到必败态的方式。
- 理解方式二:假如对手处于偶数台阶的异或为 0 的局面,他可以通过把第一个台阶的所有石子全部放到地面上,而轮到我们的时候就只能破坏这个异或为 0 的局面(因为不能移动第 0 个台阶也就是地面,只能移动其他台阶的石子),把必胜的局面(异或为非 0)给了对手。但是作为先手方就是要避免这种情况出现,采取最优策略是不会让对手偶数阶台阶异或为 0,且一号台阶有石子的。那么,当先手方遇到初始偶数阶台阶异或为 0 情况时,拿光 1 号台阶的石子到地面保持自己的必败态?这是不现实的,这就不叫做最优策略,而是最蠢策略了。所以通过判断偶数阶台阶异或为 0 的操作,必胜态与必败态之间的转化是困难的。中间绕的弯很多,暂时没找到很好的策略来解释这个问题。
代码:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int res = 0, cnt = 0;
while (n --) {
int a;
cin >> a;
++ cnt;
if (cnt % 2) res ^= a;
}
puts(res ? "Yes": "No");
return 0;
}
3. Mex运算与SG函数
Mex运算:
设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即: mex(S) = min{x}, x属于自然数,且x不属于S
SG函数:
在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1,y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即: SG(x) = mex({SG(y1), SG(y2), …,SG(yk)}) 特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。
- SG(k)有k个后继节点,且分别为 0~k-1
- 非 0 可以走向 0
- 0 只能走向非 0
- 多个独立局面的SG值等于这些局面的SG值的异或值
例:若
S
=
[
2
,
5
]
S=[2,5]
S=[2,5] 表示每次只能取 2 个或 5 个石子,
h
=
10
h=10
h=10 表示公有 10 个石子,则有展开形式为:
定理: 对于一个图 G
,如果 SG(G) != 0
,则先手必胜,反之必败
证明:
- 如果
SG(G) = !0
,则根据性质 2,先手必可以走向 0,所以后手遇见的是 0 的局面,然而 0 局面仅对应终点局面或者只能走向非 0 局面。故先手必胜。
定理: 对于 n
个图,如果 SG(G1) ^ SG(G2) ^ ... ^ SG(Gn) ^ != 0
,则先手必胜,反之必败。
证明:
- 类比
Nim
游戏 - 当
SG(Gi) = 0
时,xor = 0
,显然先手必败,简单理解为所有图中均无石子可取。终止状态必为目前这个局面,但是当前局面并不一定为终止状态 - 当
xor = x = !0
,因为肯定存在一个SG(Gi) ^ x < SG(xi)
,而根据SG()
性质 1,SG(k)
可以走到0~k-1
的任何一个状态。因此,必定可以从SG(xi)
走到SG(xi)^x
状态,那么使得xor = 0
- 当
xor = 0
,类比Nim
游戏,每移动一个节点,对应的SG
值必然减小,且xor != 0
。即若xor = 0
,则说明移动的那个节点的值并没有变化,即从SG(k)
变成了k
,但是这与SG
函数性质 1 矛盾。这个证明完全与Nim
游戏证明方式相同。
4. 集合 - Nim 游戏+变种题
重点: SG 函数,dfs 求解 SG 值
采用记忆化搜索来求解 SG
值,在此还需理解石子相同那么 SG
值也相同的原因
- 因为取法方式唯一,那么所能造成的局面是固定的
- 不论是它作为起点,后序的分支局面都已经确定了
- 还是它作为某个分支中的一个局面,那么它后序的局面也已经确定了
- 所以,相同石子的 SG 值相等。所以可以采用记忆化搜索来加速搜索效率
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 105, M = 1e4+5;
int n, k;
int s[N], f[M]; // s存取石子的方法,f存取所有取法对应的sg值
// dfs求取sg结果
int sg(int x) {
if (f[x] != -1) return f[x];
unordered_set<int> S; // 哈希表存所有可以到的局面
for (int i = 0; i < k; ++i)
if (x >= s[i])
S.insert(sg(x - s[i]));
for (int i = 0 ; ; ++i)
if (!S.count(i))
return f[x] = i;
}
int main() {
cin >> k; // s个固定取法
memset(f, -1, sizeof f); // 记忆化搜索数组初始化
for (int i = 0; i < k; ++i) cin >> s[i];
cin >> n; // 每堆石子的数量
int res = 0;
for (int i = 0; i < n; ++i) {
int x;
cin >> x;
res ^= sg(x);
}
puts(res ? "Yes" : "No");
return 0;
}
5. 拆分 - Nim 游戏+变种题
重点: SG 函数,dfs 求解 SG 值
思路:
- 这个相较于上题来讲,本题每一堆可以变成不大于原来那堆的任意大小的两堆。这样保证了最大石子可以一直变小,则一定会结束
a
石子分解成b
石子和c
石子,肯定需要保证b
和c
均小于a
。且为了保证不进行重复计算,枚举的时候b
大于等于c
注意下就行了- 则此时局面
a
,拆分成了两个独立局面b
和c
,由于SG
函数性质,多个独立局面的SG
值等于这些局面SG
值的异或 - 所以,哈希表中存的就是
sg(b) ^ sg(c)
这是本题与上题的唯一区别
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 105;
int n;
int f[N];
int sg(int x) {
if (f[x] != -1) return f[x];
unordered_set<int> S;
for (int i = 0; i < x; ++i)
for (int j = 0; j <= i; ++j)
S.insert(sg(i) ^ sg(j));
for (int i = 0; ; ++i)
if (!S.count(i))
return f[x] = i;
}
int main() {
cin >> n;
memset(f, -1, sizeof f);
int res = 0;
for (int i = 0; i < n; ++i) {
int x;
cin >> x;
res ^= sg(x);
}
puts(res ? "Yes" : "No");
return 0;
}