2024/6/28UPD CSDN又擅自给我改VIP文章:(
我也不知道是脑子抽了还是咋地,假期的研究性学习竟然选择了博弈论。既然选择了开题,那还是来浅浅的写一些吧。
在此感谢 @繁凡さん 大佬提供的思路,更推荐大家去看他的《博弈论全家桶》(ACM / OI)(超全的博弈论 / 组合游戏大合集) 一文。
0x00 前置概念
0x01 公平组合游戏 (ICG)
一个游戏为公平组合游戏应当满足以下条件:
- 由两名玩家轮流行动;
- 在任何情况下,合法的行动操作与当前执行操作的玩家无关;
- 游戏的任何状态不可以多次到达;
- 游戏以其中一名玩家不能再执行操作为结束;
- 游戏一定会在有限步骤内以非平局结束。
依以上定义,我们生活中常见的围棋,飞行棋,五子棋等都不是公平组合游戏。
下面我们将介绍两种常见的公平组合游戏。
0x02 判负
当轮到某玩家执行操作时,不能够再做出任何操作时,则判定该玩家输掉了本场游戏。
0x03 先手必胜 & 先手必败
- 先手必胜:先手在执行操作后可以将下一状态变为必败态 (该状态的后继状态中存在一个必败态);
- 先手必败:先手在执行操作时,无论如何操作,都只能进入一个必胜态 (该状态的后继状态中只存在必胜态)。
通常我们规定先手为 Alice 后手为 Bob (我也不知道为啥不是 Alex 和 Briant)。
对于以上两个状态我们有如下
定理
- 一个状态为必败态有且仅当他没有后继状态或者他的后继状态都是必胜态
- 一个状态为必胜态有且仅当至少有一个必败态为他的后继状态;
0x10 几个经典的公平组合游戏
0x11 有向图游戏 (博弈图)
定义
给定一个有向无环图 (DAG) ,在一个唯一的起点,上放置一枚棋子,两名玩家轮流移动棋子 (沿着有向边方向),以其中一名玩家无法移动为结束。
性质
任何一个公平组合游戏都可以转化为有向图游戏。(这也是我把它放到第一个的原因)
实现方法: 将每个游戏状态抽象为博弈图中的一个点,以状态之间的转移为有向边构建有向无环图,以初始状态为起点,出度为零的点即为终止状态。
必胜点(N点)与必败点(P点)
必胜点 (Next Player) :下一个执行操作的选手将取胜 (进入必胜状态) 的点。
必败点 (Previous Player):上一个执行操作的选手将取胜 (进入必胜状态) 的点。
定理
- 所有的中止节点都为必败点;
- 从任何一个必胜点出发,必定能够通过一次操作到达一个必败点;
- 从任何一个必败点出发,通过一次操作无论如何都只能到达必胜点。
有向图的核
如果称一张有向无环图 (DAG) <V, E> 的一个点集 (V 的子集) S 为该有向图的核,有且仅当满足以下条件:
- S 是一个点独立集 (该集合内的点都不能互相达到);
- ∁ V S \complement_V S ∁VS 中的点都能通过一次操作到达 S 中的点。
注意: 一张有向图可能有很多个核。
结论
一个核内的所有点都是必败点。
证明
若此时 Alice 在 S 内,由于 S 是独立集,Alice 不论如何操作都只能到达 ∁ V S \complement_V S ∁VS 中的点。
而根据定义, ∁ V S \complement_V S ∁VS 中的点都可以一步到达 S 中的点,所以 ∁ V S \complement_V S ∁VS 中不存在必败点,而 Bob 可以始终将棋子移到 S 中直到 Alice 无路可走。
0x12 Nim游戏
定义
有 n 堆物品,每堆有 a i a_i ai 件,由两名玩家轮流取物,一次可以取走 k ( k ⩾ 1 k \geqslant 1 k⩾1) 件,取走最后一件物品的玩家获胜。若两名玩家都采取最优策略,则先手是否必胜?
概念
- 局面:指在 Nim 游戏中,游戏中的所有状态;
- 先手与后手:在 Nim 游戏中,所有的先手和后手都是在整局游戏的范围内探讨的。也就是说先手是指在整局游戏中第一个行动的玩家;
- 必败局面:即无论如何操作都将输掉游戏的局面;
- 最优策略:指在完全不失误的情况下,尽可能使对手陷入必败状态的策略。
解决方案
结论
若当前状态为先手必败态,当且仅当满足 x o r i = 1 n a i = = 0 xor_{i = 1}^{n}a_i == 0 xori=1nai==0。(当然,如果你希望你的程序喜提TLE的话,你也可以建图硬解)。
证明
对于这个判定条件,我们可以将其拆分为三步进行证明:
- 对于结束状态满足上述条件:
最终状态有 ∀ a i = = 0 , ( i ∈ [ 1 , n ] ) \forall a_i == 0, (i \in [1, \; n]) ∀ai==0,(i∈[1,n]),因而有 x o r i = 1 n a i = = 0 xor_{i = 1}^{n}a_i == 0 xori=1nai==0,证毕;
- 对于任意状态满足 x o r i = 1 n a i = = 0 xor_{i = 1}^{n}a_i == 0 xori=1nai==0,如果它有后继状态,那么它的后继状态只能为满足 x o r i = 1 n a i ≠ 0 xor_{i = 1}^{n}a_i \neq 0 xori=1nai=0 的状态:
考虑反证,我们假设存在一种取物方法使得某一堆物品 a i a_i ai 变为 a i ′ a_i' ai′,使得最终有 a 1 x o r a 2 x o r … x o r a i ′ x o r … x o r a n = = 0 a_1 \; xor \; a_2 \; xor \; \dots \; xor \; a_i' \; xor \; \dots \; xor \; a_n == 0 a1xora2xor…xorai′xor…xoran==0。
而我们知道 a 1 x o r a 2 x o r … x o r a i x o r … x o r a n = = 0 a_1 \; xor \; a_2 \; xor \; \dots \; xor \; a_i \; xor \; \dots \; xor \; a_n == 0 a1xora2xor…xoraixor…xoran==0 两式相异或得 a i x o r a i ′ = = 0 a_i \; xor \; a_i' == 0 aixorai′==0。由异或的性质我们可知此时 a i = = a i ′ a_i == a_i' ai==ai′,不满足游戏任意状态不可多次到达的定义,故这样的取物方法不存在,即这样的后继状态不存在,证毕;
- 对于任意状态满足 x o r i = 1 n a i ≠ 0 xor_{i = 1}^{n}a_i \neq 0 xori=1nai=0,那么一定存在一个满足 x o r i = 1 n a i = = 0 xor_{i = 1}^{n}a_i == 0 xori=1nai==0 的后继状态:
令 x o r i = 1 n a i = = k , ( k ≠ 0 ) xor_{i = 1}^{n}a_i == k, \; (k \neq 0) xori=1nai==k,(k=0),我们需要存在一种取物方法使得某一堆物品 a i a_i ai 变为 a i ′ a_i' ai′,使得最终有 a 1 x o r a 2 x o r … x o r a i ′ x o r … x o r a n = = 0 a_1 \; xor \; a_2 \; xor \; \dots \; xor \; a_i' \; xor \; \dots \; xor \; a_n == 0 a1xora2xor…xorai′xor…xoran==0。那么显然有 a i = = a i ′ x o r k , ( a i > ( a i ′ x o r k ) ) a_i == a_i' \; xor \; k, \; (a_i \gt (a_i' \; xor \; k)) ai==ai′xork,(ai>(ai′xork))。
若 k k k 的最高位为 p p p,则一定存在至少一个且一定是奇数个 a i a_i ai 使得 ( a i ≫ p ) & 1 = = 1 (a_i \gg p) \; \And \; 1 == 1 (ai≫p)&1==1 (第 p 位是 1),而由异或的性质可知,对于这样的 a i a_i ai 一定满足 a i > ( a i ′ x o r k ) a_i \gt (a_i' \; xor \; k) ai>(ai′xork)。
那么我们只需选择其中一个满足 ( a i ≫ p ) & 1 = = 1 (a_i \gg p) \; \And \; 1 == 1 (ai≫p)&1==1 的一堆物品,取走 a i − ( a i x o r k ) a_i - (a_i \; xor \; k) ai−(aixork) 件即可,证毕。
- 综上所述,由以上三点我们可以知道,如果当前状态满足 x o r i = 1 n a i = = 0 xor_{i = 1}^{n}a_i == 0 xori=1nai==0,则 Alice 只能操作使得 x o r i = 1 n a i ≠ 0 xor_{i = 1}^{n}a_i \neq 0 xori=1nai=0,而 Bob 可以再次操作使得 x o r i = 1 n a i = = 0 xor_{i = 1}^{n}a_i == 0 xori=1nai==0 直到变为最终状态,Alice 被判负。则 Alice 必输,即先手必输。反之同理,得证。
代码实现
模板:洛谷P2197
//省略头文件和快读
int T;
int n;
int a[MAXN];
int main()
{
T = inpt();//多组测试用例
while(T--) {
n = inpt();
for(int i = 1; i <= n; i++)
a[i] = inpt();
int tmp = 0;
for(int i = 1; i <= n; i++)
tmp ^= a[i];
if(tmp)
puts("Yes");
else
puts("No");
}
return 0;
}
0x13 Bash游戏
定义
有一堆有石子数量为 n,两名玩家轮流取,每人每次至少取 1 个,最多取 m 个,取走最后一个石子的玩家获胜。问先手是否必胜。
解决方案
结论
若 ( m + 1 ) ∣ n (m + 1) | n (m+1)∣n 则先手必败,否则先手必胜。
证明
考虑分为以下两步证明:
- 当 n = = m + 1 n == m + 1 n==m+1 时,由于最多取 m 个,因而显然此时 Alice 无法取完且无论取走多少个,Bob 都能取完,故Alice必败,即先手必败;
- 当 ( m + 1 ) ∣ n (m + 1) | n (m+1)∣n 时,同理,Alice 无论取走多少个都只能达到一个 ( m + 1 ) ∤ n (m + 1) \nmid n (m+1)∤n 的状态,而 Bob 则可以恢复到另一个 ( m + 1 ) ∣ n (m + 1) | n (m+1)∣n 的状态直到 n = = m + 1 n == m + 1 n==m+1,因而 Alice 必败,即先手必败。反之则 Alice 可以达到一个 ( m + 1 ) ∣ n (m + 1) | n (m+1)∣n 的状态使 Bob 必败,即先手必胜,得证。
代码实现
(暂未找到裸的模板题)
//省略头文件和快读
int n, m;
int main()
{
n = inpt(), m = inpt();
if(n % (m + 1))
puts("Alice win");
else
puts("Bob win");
return 0;
}
0x14 Wythoff游戏
定义
有两堆石子,两名玩家轮流取石子,可以只取一堆或两堆取相同数目,取走最后一个石子的玩家为胜。问先手是否必胜。
概念
奇异节点:将状态放在平面直角坐标系内,x 轴 y 轴分别代表两堆石子的数量。我们将先手必败点称为奇异节点 (如 (0, 0), (1, 2) 等)。
解决方案
结论
令两堆石子的数量分别为 a 和 b,其中 a < b a < b a<b,则当前状态为先手必败有且仅当满足以下条件:
- ( b − a ) ∗ 5 + 1 2 = = a (b - a) * \frac{\sqrt{5} + 1}{2} == a (b−a)∗25+1==a
证明
没学懂,长大再学QwQ (要用 Beatty 定理)
代码实现
模板:2017 ACM ICPC dalian C (简化版)
原题数据范围开到了
1
0
100
10^{100}
10100 需要用特殊的高精 (听说普通 C++ 的高精要炸,真可怕QwQ)
模板2: P2252 [SHOI2002]取石子游戏|【模板】威佐夫博弈
//省略快读和头文件
int a, b;
int main()
{
a = inpt(), b = inpt();
if(a > b)
swap(a, b);
int tmp = (b - a) * ((sqrt(5.0) + 1.0) / 2.0);
if(tmp != a)
puts("Alice win");
else
puts("Bob win");
return 0;
}
0x15 Fibonacci游戏
定义
有一堆数量为 n (n > 1) 的石子,第一次至少取一颗,最多取 n - 1 颗。此后每一轮至少取一个,最多取上一个玩家的 2 倍,取走最后一个石子的玩家获胜。问先手是否必胜。
解决方案
结论
如名字所见,当前状态为先手必败态,有且仅当满足以下条件:
- n 是斐波那契数。
证明
- 必要性用数学归纳法,具体的就是拆分成两个斐波那契数,Bob 必定能取到最后一个 。
- 充分性利用定理任意任何正整数可以表示为若干个不连续的Fibonacci数之和 (这两句话是CV @繁凡さん 大佬的,长大再学)。
代码实现
(暂未找到裸的模板题)
//省略快读和头文件
int fib[MAXN];
unordered_map<int, bool> mp;
int main()
{
fib[1] = 1, fib[2] = 1;
for(int i = 3; i <= MAXN - 5; i++) {
fib[i] = fib[i - 1] + fib[i - 2];
mp[fib[i]] = true;
}
int n = inpt();
if(!mp[n])
puts("Alice win");
else
puts("Bob win");
return 0;
}
0x20 SG函数
0x21 Mex运算
定义
m
e
x
(
S
)
mex(S)
mex(S) 为不属于非负集合
S
S
S 的最小非负整数。即
m
e
x
(
S
)
=
m
i
n
x
∈
N
,
x
∉
S
{
x
}
mex(S) = min_{x \in N, \; x \notin S} \{x\}
mex(S)=minx∈N,x∈/S{x}。
e
g
.
m
e
x
(
1
,
5
,
7
,
9
)
=
0
,
m
e
x
(
0
,
1
,
2
,
7
)
=
3
eg. \\ mex({1, 5, 7, 9}) = 0, \; mex({0, 1, 2, 7}) = 3
eg.mex(1,5,7,9)=0,mex(0,1,2,7)=3
0x22 SG函数
定义
SG函数是一个递归含义下定义的一个函数。
对于一个公平组合游戏,我们规定对于状态
x
x
x,其后继状态集合为
s
u
b
x
sub_x
subx,则有:
S
G
(
x
)
=
m
e
x
y
∈
s
u
b
x
{
S
G
(
y
)
}
SG(x) = mex_{y \in sub_x}\{SG(y)\}
SG(x)=mexy∈subx{SG(y)}
特别的,我们规定对于中止状态
t
t
t 有
S
G
(
t
)
=
0
SG(t) = 0
SG(t)=0,又有规定整个游戏
G
G
G 的 SG 函数指为起始状态
s
s
s 的 SG 函数值,即
S
G
(
G
)
=
S
G
(
s
)
SG(G) = SG(s)
SG(G)=SG(s)。
定理
- 如果某个状态为必胜态,当且仅当它的 SG 函数值大于 0;
- 如果某个状态为必败态,当且仅当它的 SG 函数值等于 0。
证明
证明可分为以下三步:
- 终止状态的 SG 值为 0;
- 如果一个状态的 SG 函数值大于 0,说明他的所有后继状态中必定有至少一个 SG 函数值为 0。(那么这个后继状态为终止状态时先手必胜);
- 如果一个状态的 SG 函数值等于 0,那么他的所有后继状态的 SG 函数值都大于 0。即此时 Alice 只能到达一个 SG 函数值大于 0 的状态,而由 证明2 得 Bob 必定可以再次使局面进入一个等于 0 的状态,直到进入终止状态,则 Alice 必败,定理 2 得证。由此定理 1 的正确性也很显然了,证毕。
0x23 公平组合游戏的和
对于 n 个相互独立公平组合游戏,我们定义他们的和 S 为他们的 SG 函数值的异或和,即 S G ( S ) = x o r i = 1 n S G ( G i ) SG(S) = xor_{i = 1}^{n} \; SG(G_i) SG(S)=xori=1nSG(Gi)。
应用
例如对于一场有向图游戏,如果场上有 n 个棋子 (这些棋子可以相互重合)。那么这场游戏的 SG 函数值可以看作 n 场不同的单棋子有向图游戏的和。那么此时先手必败,当且仅当满足 x o r i = 1 n S G ( p i ) = = 0 xor_{i = 1}^{n} \; SG(p_i) == 0 xori=1nSG(pi)==0。通常,这样的将一个游戏拆分为多个子游戏的方式也被称为SG(Sprague−Grundy)定理。
0x24 Nim转化
注:标题中的名字是我自己取的,如果有更加专业的名字拜请您指出。
结论
对于任意的简单 SG-组合游戏,我们实际上都可以把它转化为 Nim游戏。具体转化如下:
- 对于一个 SG 函数值为 k 的游戏,我们可以把它转化为一堆数量为 k 的石子。
- 同样的,对于每个子游戏我们也可以做如上拆分,即拆分为 n 堆石子,数量分别为其 SG 函数值。
由这个结论我们可以知道,对于一个游戏我们只关注他的 SG 值而不关心他的具体实现。这启示我们联想到使用记忆化的方式来降低复杂度。
例题
集合-Nim游戏
题面
有 n 堆石子,数量分别为 a i a_i ai,给定一个整数集 S,两人轮流取石子,每次可以取走 S 中的数量的石子,无法操作为负。问先手是否必胜。
解决方案
首先我们把它转化成有向图游戏,也就是我们首先需要建立博弈图:
(下面我们以其中一堆数量为 10,集合为 {2, 5} 为例)
上图中 10 是起点,通过计算其 SG 函数,我们得出当前状态为先手必胜。
同理,如果有多堆石子,则有多个起点,我们可以通过记忆化的方式降低复杂度。
代码实现
(我没钱买ACWing的课程,没拿到测试用例,不保证代码正确性)
//省略快读和头文件
int m, s[MAXN];
int n;
int ans = 0;
int sg[MAXM];
int SG(int x)
{
if(~sg[x])
return sg[x];
unordered_map<int, bool> mp;
for(int i = 1; i <= m; i++)
if(x >= s[i])
mp[SG(x - s[i])] = true;
for(int i = 0; ; i++)
if(!mp[i])
return sg[x] = i;
}
int main()
{
m = inpt();
for(int i = 1; i <= m; i++) {
s[i] = inpt();
}
n = inpt();
memset(sg, -1, sizeof(sg));
for(int i = 1; i <= n; i++) {
int a = inpt();
ans ^= SG(a);
}
if(ans)
puts("Yes");
else
puts("No");
return 0;
}
0x30 SG函数的拓展
0x31 Anti-SG游戏
顾名思义,Anti-SG 游戏就是与 SG 游戏规则相反的游戏。
引子——Anti-Nim游戏
定义
有 n 堆石子,每次可以选择一堆取 k (k > 0) 个石子,取走最后一个石子的为负,问先手是否必胜。
解决方案
结论
当前状态为先手必胜,有且仅当满足下列条件中的任意一个:
- 每一堆的数量都是 1,且 x o r i = 1 n S G ( a i ) = = 0 xor_{i = 1}^{n}SG(a_i) == 0 xori=1nSG(ai)==0。
- 存在至少一堆数量大于 1,且 x o r i = 1 n S G ( a i ) ≠ 0 xor_{i = 1}^{n}SG(a_i) \neq 0 xori=1nSG(ai)=0
证明
对于条件一,由于 k > 0,所以只有一人取一个,显然此时异或和为 0 时先手必胜,证毕。
对于条件二,我们拆分为两步进行证明
- 只有一堆个数大于 0,显然此时异或和必定不等于 0,满足条件二。而此时如果除去这一堆异或和为 0,则取得只剩 1 个,此时先手必胜,如果不为 0,则直接取完,显然此时仍然先手必胜,证毕;(即只有一堆大于零则先手必胜)
- 至少有两堆个数大于 0,我们再次拆分成两步证明:
i. 异或和为 0 时,先手无论如何操作,都只能到达一个异或和不为 0 且至少有一堆大于 0 的状态,而此时的后手要么已经进入了 条件1 的状态,要么可以再次恢复到异或和为 0 的状态直到进入 条件1 状态。此时先手必败;(即异或和为 0,先手必败)
ii. 异或和大于 0 时,显然先手可以通过一次操作使后手陷入 i 的状态中,则此时 先手必胜,证毕。(即异或和大于 0,先手必胜)
例题
模板: 洛谷P4279
//省略快读和头文件 (顺手拿下了洛谷第五最优解)
int T;
int n;
int all1, xum;
int main()
{
T = inpt();
while(T--) {
n = inpt();
all1 = 1, xum = 0;
for(int i = 1; i <= n; i++) {
int a = inpt();
all1 &= (a == 1);
xum ^= a;
}
if(all1 ^ xum)
puts("John");
else
puts("Brother");
}
return 0;
}
正题——Anti-SG游戏
然而,我们上面所推出的结论实际上只针对于 Anti-Nim 游戏实用。@繁凡さん 大佬给出的解释是因为我们在证明 SG 函数性质时,用到了这样一个性质:SG值为0的局面不一定为终止局面 。 而我始终是没有太明白这一句话到底是什么意思,只好先引用到这里,看看以后能不能看懂。
定义
与 Anti-Nim 游戏相似的,Anti-SG 游戏便是除了第一个无路可走的玩家获胜这一个规则不一样,其他规则相同的 SG 游戏。
解决方案
定理
为了解决这一类问题,我们引入了 SJ定理:
对于当前状态先手必胜当且仅当满足以下条件的其中一个:
- 当前游戏的 SG 函数值为 0,且其中所有子游戏的 SG 函数值都小于等于 1。
- 当前游戏的 SG 函数值大于 0,且其中至少有一个子游戏的 SG 函数值大于 1。
证明
这里参考了 《组合游戏略述——浅谈SG游戏的若干拓展及变形》贾志豪IOI2009国家集训队论文 中的证明方法,只是改成了我自己的语言罢了:
我们假设这个定理是正确的,那么我们只需要考虑分为四种情况进行证明以下三点即可:
- 所有终止状态都是先手必胜态 (显然的);
- 任何一个先手必败态 (情况1和情况2) 的后继状态中都只有先手必胜态;
- 任何一个先手必胜态 (情况3和情况4) 的后继状态中都至少包含一个先手必败态。
(其实我觉得这三点看上去都很显然)
情况1
—— 当前状态的 SG 函数值为 0 且其至少有一个子游戏的 SG 函数值大于 1
由 SG 函数的定义我们可以知道,当前状态的所有后继状态的 SG 函数值都大于 0。
而且由于多个游戏的和的定义,我们可以知道当前游戏的子游戏中至少有 2 个的 SG 函数值大于 1。
而因为我们每次只能修改一个子游戏的状态 (SG 值),因此他的所有后继状态中都至少有一个子游戏的 SG 值大于 1,而且其和状态 SG 值大于 0。这样的状态是先手必胜的,故而这样的当前状态是先手必败的。
情况2
—— 当前状态的 SG 函数值大于 0 且其没有任何一个子游戏的 SG 函数值大于 1
同样由多个游戏的和的定义,我们可以知道当前状态一定有奇数个子游戏的 SG 值为 1。
这样的状态下我们有两种转移方式
- 将其中一个子游戏转移为一个 SG 函数值大于 1 的状态:
此时由于之前的状态中没有 SG 值小于等于 1 的子游戏,因此这样的后继状态显然 SG 值大于 1 且有 1 个 SG 值大于 1 的子游戏,我们知道这样的状态是先手必胜的; - 将其中一个子游戏转移为 SG 函数值等于 1 或 0 的状态:
这样转移之后我们可以发现这样的后记状态有偶数个子游戏的 SG值 等于 1,那么这个状态的 SG 值等于 0 且没有任何一个子游戏的 SG 值大于 1,显然这样的状态也是先手必胜的。
因此当前状态是先手必败的。
由情况1和情况2,证明2得证。
情况3
—— 当前状态的 SG 函数值大于 0 且其至少有一个子游戏的 SG 函数值大于 1
这样的情况仍然可以分为两种状况讨论:
- 只有 1 个 SG 函数值大于 1 的子游戏:
与 Anti-Nim 游戏相似的,我们可以通过这个来控制场上是奇数个 SG 值为 1 的子游戏,显然这样的状态先手必胜; - 至少有 2 个 SG 函数值大于 1 的子游戏:
同理可知,由于一次只能修改一个子游戏,于是其后继状态是存在先手必败的,那么这样的状态先手必胜。
情况4
—— 当前状态的 SG 函数值为 0 且其没有任何一个子游戏的 SG 函数值大于 1
这种情况要么是都是 0 游戏结束先手必胜,要么是偶数个 1 同样先手必胜
由情况3和情况4,证明3得证。
由此 SJ 定理得证。
一段我没读懂的引用:
实际上,聪明的读者可能会发现,我们在SJ定理中给出的附加条件 “规定当局面中所有的单一游戏的SG值为0时,游戏结束”过于严格, 完全可以替换成“当局面中所有的单一游戏的SG值为0时,存在一个单 一游戏它的SG函数能通过一次操作变为1”。
笔者为什么要将限制条件设制成这样?
因为笔者发现这样可以出题,我们可以将题目模型设成这样:游戏 中存在一个按钮,游戏双方都可以触动按钮,当其中一个人触动按钮时,触动按钮的人每次必须移动对方上次移动的棋子。如果触动按钮的人能 保证他能够使得对方无路可走,那么他同样获胜!
——以上来自 @繁凡さん 大佬
0x32 Multi-SG游戏
Multi-SG游戏就是在符合拓扑原理的前提下,一个子游戏的后继可以是几个规模更小的子游戏。
引子——Multi-Nim游戏
定义
在普通的 Nim游戏的基础上,加入了可以将一堆石子分为不少于两堆的非空石子堆。
例子
借用 @繁凡さん 大佬的例子,SG(3) 的后继状态有{SG(0) = 0, SG(1) = mex{SG(0)} = 1, SG(2) = 2 = mex{SG(0), SG(1)} = 2, SG(1, 2) = SG(1) xor SG(2)},其中 SG(1, 2) 表示这两个游戏的和,因此 SG(3) = 4。
规律
通过数学归纳法或者打表我们可以发现,对于分成两堆的 Multi-Nim游戏有如下规律:
S
G
(
x
)
=
{
x
−
1
if
x
%
4
=
=
0
x
otherwise
x
+
1
if
x
%
4
=
=
3
SG(x) = \begin{cases} x - 1&\text{if } x \% 4 == 0 \\ x &\text{otherwise} \\ x + 1 &\text{if } x \% 4 == 3 \end{cases}
SG(x)=⎩
⎨
⎧x−1xx+1if x%4==0otherwiseif x%4==3
打表程序
//省略快读和头文件
int sg[MAXN];
bool vis[MAXN];
int main()
{
sg[0] = 0, sg[1] = 1;
for(int i = 1; i <= MAXN - 5; i++) {
memset(vis, false, sizeof(vis));
for(int j = 1; j <= i; j++)//取 j 个
vis[sg[i - j]] = true;
for(int j = 1; j < i; j++)//拆分为 i - j 和 i
vis[sg[i - j] ^ sg[j]] = true;
int tmp = 0;
for(; vis[tmp]; tmp++);
sg[i] = tmp;
}
for(int i = 1; i <= MAXN - 5; i++)
printf("SG(%d) = %d\n", i, sg[i]);
return 0;
}
例题1
解决方案
模板题,套用上述结论即可
代码实现
//省略快读和头文件
int T;
int n;
int main()
{
T = inpt();
while(T--) {
n = inpt();
int res = 0;
for(int i = 1; i <= n; i++) {
int a = inpt();
if(a % 4 == 0)
res ^= a - 1;
else if(a % 4 == 3)
res ^= a + 1;
else
res ^= a;
}
if(res)
puts("Alice");
else
puts("Bob");
}
return 0;
}
例题2
变式:【HDU 5795】A Simple Nim
将 例题1(模板) 中的拆分成两堆改为拆分为三堆。
解决方案
同样打表可得:
S
G
(
x
)
=
{
x
−
1
if
x
%
8
=
=
0
x
otherwise
x
+
1
if
x
%
8
=
=
7
SG(x) = \begin{cases} x - 1&\text{if } x \% 8 == 0 \\ x &\text{otherwise} \\ x + 1 &\text{if } x \% 8 == 7 \end{cases}
SG(x)=⎩
⎨
⎧x−1xx+1if x%8==0otherwiseif x%8==7
代码实现
打表代码
//将拆分部分改为这样即可
for(int j = 1; j < i; j++)//拆分为 i - j 和 i
for(int k = j; j + k < i; k++)//拆分为 i - j - k 、 i 和 k
vis[sg[i - j - k] ^ sg[j] ^ sg[k]] = true;
题目代码
//将关键部分改为这样即可
for(int i = 1; i <= n; i++) {
int a = inpt();
if(a % 8 == 0)
res ^= a - 1;
else if(a % 8 == 7)
res ^= a + 1;
else
res ^= a;
}
正题——Multi-SG游戏
即一个游戏的后继状态中,原来的一个子游戏可能对应后继中的多个子游戏。
结论
由 SG定理可知,我们的后继状态的 SG 值为其所有子游戏的 SG 值异或和。
例题
有 n 个抽屉排成一排,按照 1~n 编号,每个抽屉中有一些物品 (可以为空),每次可以从某个抽屉中取出一件物品,复制成两份分别放到编号比这个抽屉大的一个或两个抽屉中。最终无法操作的玩家被判负。
解决方案
将分开的两个物品视为两个子游戏即可。
不过很遗憾的是,这道题是我在听一个机房大佬讲课的时候听到的,我没有测试样例,于是也没有办法测试,而且我打表并没有发现规律,于是大概是只好硬解吧。如果有人发现了规律请一定要告诉我,谢谢您了!
0x33 Every-SG问题
定义
在一张有向图上 (@繁凡さん 说是无向图,我不知道是否正确) 有一些棋子,两个玩家轮流操作,每人每次必须将所有可以移动的棋子都进行移动,最后不能移动的人输。
解决方案
转换题面,就是无论前面的棋子输赢与否,只要最后一个棋子胜利就判定胜利。
接下来又是一段我没太看懂的引用,希望以后来看能看懂:
这样的话,能赢得游戏必须赢
因为两个人都采用最优策略,因此当一个人知道某一个游戏一定会输的话,它一定会尽力缩短游戏的时间,当它知道某一个游戏一定会赢的话,一定会尽力延长游戏的时间
Every-SG游戏与普通SG游戏最大的不同就是它多了一维:时间
对于SG值为0的点,我们需要知道最少需要多少步才能走到结束,对于SG值不为0的点,我们需要知道最多需要多少步结束
这样我们用step变量来记录这个步数
0xff 结语
其实在博弈论方面还有很多像翻硬币游戏,无向图删边游戏,经典组合游戏拓展,巴什博奕的扩展——k倍动态减法游戏,尼姆博弈的三种扩展寻找必败态解题甚至还有不平等博弈问题等问题还有待我去探讨,但由于时间原因和我的当下水平的原因只好暂时搁置了,希望以后大概回来填坑吧。
祝你好运。
摘一段我喜欢的话(感谢王老师的提笔~)