博弈论分类
博弈论的游戏主要分为:
-
公平组合游戏:
游戏有两个人参与,二者轮流做出决策,双方均知道游戏的完整信息;
任意一个游戏者在某一确定状态可以作出的决策集合,只与当前的状态有关,而与游戏者无关;
游戏中的同一个状态不可能多次抵达,游戏以玩家无法行动为结束,且游戏一定会在有限步后以非平局结束。 -
非公平组合游戏
在非公平组合游戏中,游戏者在某一确定状态可以做出的决策集合与游戏者有关。大部分的棋类游戏都不是公平组合游戏,如国际象棋、中国象棋、围棋、五子棋等(因为双方都不能使用对方的棋子)。 -
反常游戏
胜者为第一个无法行动的玩家。以 Nim 游戏为例,Nim 游戏中取走最后一颗石子的为胜者,而反常 Nim 游戏中取走最后一刻石子的为败者。
NIM游戏
游戏定义:给定 n n n堆物品,第 i i i堆物品有 A i A_i Ai个。两名玩家轮流行动,每次可以人选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一样物品的人获胜。两人都采取最优策略,稳先手能否必胜。
可以看到NIM游戏是一个公平组合游戏
NIM游戏不存在平局,只有先手必胜和先手必败
先手必胜:只要存在某种行动,使得后手一定会输掉游戏
先手必败:无论采取何种行动,先手都会输掉游戏
定理
先手必胜:当且仅当
A
1
⊕
A
2
⊕
.
.
.
⊕
A
n
≠
0
A_1 \oplus A_2 \oplus ... \oplus A_n \neq 0
A1⊕A2⊕...⊕An=0
先手必败:当且仅当
A
1
⊕
A
2
⊕
.
.
.
⊕
A
n
=
0
A_1 \oplus A_2 \oplus ... \oplus A_n = 0
A1⊕A2⊕...⊕An=0
⊕
\oplus
⊕表示异或(XOR)
证明
结论1
当场上不存在石子时,此时 A 1 ⊕ A 2 ⊕ . . . ⊕ A n = 0 ⊕ 0 ⊕ . . . ⊕ 0 = 0 A_1 \oplus A_2 \oplus ... \oplus A_n = 0 \oplus 0 \oplus... \oplus 0=0 A1⊕A2⊕...⊕An=0⊕0⊕...⊕0=0
结论2
当场上石子满足 A 1 ⊕ A 2 ⊕ . . . ⊕ A n ≠ 0 A_1 \oplus A_2 \oplus ... \oplus A_n \neq 0 A1⊕A2⊕...⊕An=0时,一定可以在某一堆 A i A_i Ai中取若干石子,使得场上石子变成 A 1 ⊕ A 2 ⊕ . . . ⊕ A n = 0 A_1 \oplus A_2 \oplus ... \oplus A_n = 0 A1⊕A2⊕...⊕An=0
- 设 x = A 1 ⊕ A 2 ⊕ . . . ⊕ A n x=A_1 \oplus A_2 \oplus ... \oplus A_n x=A1⊕A2⊕...⊕An,则 x x x最高位的1,一定由奇数个A的最高位组成,不妨设其中一个是 A i A_i Ai
- x ⊕ A i x \oplus A_i x⊕Ai将会令 x x x的最高位变成0,而 A i A_i Ai最高位仍是1,故 x ⊕ A i < A i x \oplus A_i<A_i x⊕Ai<Ai
- 在 A i A_i Ai这堆石子中拿走 A i − ( x ⊕ A i ) A_i-(x \oplus A_i) Ai−(x⊕Ai)个石子,剩下 x ⊕ A i x \oplus A_i x⊕Ai个石子。
- 场上所有石子异或
A 1 ⊕ . . . ( x ⊕ A i ) . . . ⊕ A n A_1 \oplus ... (x \oplus A_i) ... \oplus A_n A1⊕...(x⊕Ai)...⊕An
= A 1 ⊕ . . . ( ( A 1 ⊕ . . . ⊕ A n ) ⊕ A i ) ⊕ . . . ⊕ A n = A_1 \oplus ... ((A_1 \oplus ... \oplus A_n) \oplus A_i) \oplus... \oplus A_n =A1⊕...((A1⊕...⊕An)⊕Ai)⊕...⊕An
= ( A 1 ⊕ A 1 ) ⊕ ( A 2 ⊕ A 2 ) ⊕ . . . ⊕ ( A n ⊕ A n ) = (A_1 \oplus A_1) \oplus (A_2 \oplus A_2) \oplus...\oplus (A_n \oplus A_n) =(A1⊕A1)⊕(A2⊕A2)⊕...⊕(An⊕An)
= 0 =0 =0
结论3
当场上石子满足 A 1 ⊕ A 2 ⊕ . . . ⊕ A n = 0 A_1 \oplus A_2 \oplus ... \oplus A_n = 0 A1⊕A2⊕...⊕An=0时,无论怎么取石子,都会导致场上石子变成 A 1 ⊕ A 2 ⊕ . . . ⊕ A n ≠ 0 A_1 \oplus A_2 \oplus ... \oplus A_n \neq 0 A1⊕A2⊕...⊕An=0
反证:若取后场上石子变成 A 1 ⊕ A 2 ⊕ . . . ⊕ A n = 0 A_1 \oplus A_2 \oplus ... \oplus A_n = 0 A1⊕A2⊕...⊕An=0
- 不妨设在 A i A_i Ai堆取了石子,则取后石子个数为 A i ′ A_i' Ai′
- 将取前和取后的石子状态互相异或
( A 1 ⊕ A 2 ⊕ . . . ⊕ A i ⊕ . . . ⊕ A n ) ⊕ ( A 1 ⊕ A 2 ⊕ . . . ⊕ A i ′ ⊕ . . . ⊕ A n ) = 0 (A_1 \oplus A_2 \oplus ... \oplus A_i \oplus... \oplus A_n) \oplus ( A_1 \oplus A_2 \oplus ... \oplus A_i' \oplus... \oplus A_n) = 0 (A1⊕A2⊕...⊕Ai⊕...⊕An)⊕(A1⊕A2⊕...⊕Ai′⊕...⊕An)=0 - 异或满足交换律,又因为
A
1
⊕
A
1
=
0
,
A
2
⊕
A
2
=
0
A_1 \oplus A_1=0, A_2 \oplus A_2=0
A1⊕A1=0,A2⊕A2=0…
上式化简为 A i ⊕ A i ′ = 0 A_i \oplus A_i'=0 Ai⊕Ai′=0,得到 A i = A i ′ A_i=A_i' Ai=Ai′ - 又因为每个人不能不取石子, A i ≠ A i ′ A_i \neq A_i' Ai=Ai′,矛盾
综上
当上面三个结论成立时,可以推断出
A
1
⊕
A
2
⊕
.
.
.
⊕
A
n
≠
0
A_1 \oplus A_2 \oplus ... \oplus A_n \neq 0
A1⊕A2⊕...⊕An=0使得先手必胜。
这是因为如果初始的石头满足
A
1
⊕
A
2
⊕
.
.
.
⊕
A
n
≠
0
A_1 \oplus A_2 \oplus ... \oplus A_n \neq 0
A1⊕A2⊕...⊕An=0,先手可以使石头变成
A
1
⊕
A
2
⊕
.
.
.
⊕
A
n
=
0
A_1 \oplus A_2 \oplus ... \oplus A_n = 0
A1⊕A2⊕...⊕An=0,而后手无论怎么取都会使石头变成
A
1
⊕
A
2
⊕
.
.
.
⊕
A
n
≠
0
A_1 \oplus A_2 \oplus ... \oplus A_n \neq 0
A1⊕A2⊕...⊕An=0,注意到石头取完的时候,
A
1
⊕
A
2
⊕
.
.
.
⊕
A
n
=
0
A_1 \oplus A_2 \oplus ... \oplus A_n = 0
A1⊕A2⊕...⊕An=0,那么石头取完的情况一定会出现在后手取石头的时候。
代码
#include <iostream>
using namespace std;
int n, a, b;
int main() {
scanf("%d", &n);
while(n -- ) {
scanf("%d", &a);
b ^= a;
}
if (!b) printf("No\n");
else printf("Yes\n");
return 0;
}
集合NIM游戏
和NIM游戏不同的是,每次取得石子数只能是规定的某几个数之一。
概念
要想解决这个问题,需要了解SG函数及其相关概念
Mex运算
求出最小的不属于集合S的非负整数
m
e
x
(
S
)
=
m
i
n
x
∈
N
,
x
∉
S
{
x
}
mex(S)=min_{x\in \mathbb{N}, x\notin S}\{ x\}
mex(S)=minx∈N,x∈/S{x}
例如
S
=
{
0
,
2
,
3
}
S=\{ 0,2,3 \}
S={0,2,3},则
m
e
x
(
S
)
=
1
mex(S)=1
mex(S)=1
有向图游戏
定义:给定一个有向无环图,图中有唯一的起点,在起点上放有一枚棋子。两名玩家交替把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动这判负。
任何一个公平组合游戏都可以转化成有向图游戏:每个局面相当于图的一个节点,合法行动是图的边。
SG函数
设有向图游戏中,节点
x
x
x有
k
k
k条有向边,分别到达节点
y
1
,
y
2
,
.
.
.
,
y
k
y_1,y_2,...,y_k
y1,y2,...,yk,定义
S
G
(
x
)
SG(x)
SG(x)为
x
x
x的后继节点
y
1
,
y
2
,
.
.
.
,
y
k
y_1,y_2,...,y_k
y1,y2,...,yk的
S
G
SG
SG函数值构成的集合再执行
m
e
x
mex
mex运算的结果。
S
G
(
x
)
=
m
e
x
(
{
S
G
(
y
1
)
,
S
G
(
y
2
)
,
.
.
,
S
G
(
y
k
)
}
)
SG(x)=mex(\{SG(y_1),SG(y_2),..,SG(y_k)\})
SG(x)=mex({SG(y1),SG(y2),..,SG(yk)})
整个有向图的SG函数值是起点的SG函数值。
上述定义,说白了就是找 x x x最小不能到达的数字。
以上图为例
- f f f和 c c c谁都不能到达,最小不能到的数为0。
- e e e能到 f f f的0,最小不能到的数是1。
- b b b能到 e e e的1和 c c c的0,最小不能到的数是2。
- d d d只能到 e e e的1,不能到0
- a a a只能到 d d d的0,不能到1
有向图游戏的和
设当前有
G
1
,
G
2
,
.
.
.
,
G
m
G_1,G_2,...,G_m
G1,G2,...,Gm总共
m
m
m个有向图游戏,定义有向图游戏
G
G
G,它的行动规则是任选某个有向图
G
i
G_i
Gi,并在
G
i
G_i
Gi上行动一步。
G
G
G就是有向图游戏
G
1
,
G
2
,
.
.
.
,
G
m
G_1,G_2,...,G_m
G1,G2,...,Gm的和。
G
G
G的
S
G
SG
SG函数值等于包含的各个子游戏的
S
G
SG
SG函数值异或和。
S
G
(
G
)
=
S
G
(
G
1
)
⊕
S
G
(
G
2
)
⊕
.
.
.
⊕
S
G
(
G
m
)
SG(G)=SG(G_1) \oplus SG(G_2) \oplus ... \oplus SG(G_m)
SG(G)=SG(G1)⊕SG(G2)⊕...⊕SG(Gm)
像NIM游戏的每堆石子就是一个有向图游戏 G i G_i Gi,整个NIM游戏看作有向图游戏的和。
定理
有向图的某个局面必胜,当且仅当该局面对应节点的SG函数值不等于0
有向图的某个局面必败,当且仅当该局面对应节点的SG函数值等于0
可以看到:
- 在没有出边的节点, S G = 0 SG=0 SG=0,对应着败局。
- S G ≠ 0 SG\neq 0 SG=0的节点一定能走到 S G = 0 SG=0 SG=0的节点。
- S G = 0 SG=0 SG=0的节点无论怎么走,都会走到 S G ≠ 0 SG\neq 0 SG=0的节点。
先手只要在
S
G
≠
0
SG\neq 0
SG=0的节点,一定可以让后手处于
S
G
=
0
SG=0
SG=0的节点;后手处于
S
G
=
0
SG=0
SG=0的节点,无论怎么走都会走到
S
G
=
0
SG=0
SG=0的节点;保持这样的关系,最后后手一定会走到没有出边的败局上。
可见
S
G
=
0
SG=0
SG=0的节点就是败局,
S
G
≠
0
SG\neq 0
SG=0的节点就是胜局。
有向图游戏的和证明类似NIM游戏的三个结论。
题目及思路
这题的每堆石子,都是一个有向图游戏。所有石子组成一个有向图游戏的和。
因此只需判断
S
G
(
G
)
=
S
G
(
G
1
)
⊕
S
G
(
G
2
)
⊕
.
.
.
⊕
S
G
(
G
m
)
≠
0
SG(G)=SG(G_1) \oplus SG(G_2) \oplus ... \oplus SG(G_m) \neq 0
SG(G)=SG(G1)⊕SG(G2)⊕...⊕SG(Gm)=0就是胜局。
问题的关键就是如何求一堆石子的SG函数的值,也就是有向图起点SG函数的值。
一堆10个石子,集合
S
=
{
2
,
5
}
S=\{2,5\}
S={2,5}的有向图如下:
节点内的数字代表石子剩余个数,例如10个石子有两种取法,取5个到节点5,取2个到节点8。
代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 105;
int k, n, s[N], h, ans;
int f[10005];
int dfs(int x) {
if (f[x] != - 1) return f[x];
bool flag[105];
memset(flag, 0, sizeof(flag));
for (int i = 0; i < k; i ++ ) {
if (x >= s[i]) {
flag[dfs(x - s[i])] = 1;
}
}
for (int i = 0; i < N; i ++ ) {
if (!flag[i]) return f[x] = i;
}
}
int main() {
scanf("%d", &k);
for (int i = 0; i < k; i ++ ) scanf("%d", &s[i]);
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) {
scanf("%d", &h);
memset(f, -1, sizeof(f));
int sg = dfs(h);
ans ^= sg;
}
if (ans) printf("Yes\n");
else printf("No\n");
return 0;
}
也可以用unordered_set代替flag数组
int dfs(int x) {
if (f[x] != - 1) return f[x];
unordered_set<int> num;
for (int i = 0; i < k; i ++ ) {
if (x >= s[i]) {
num.insert(dfs(x - s[i]));
}
}
for (int i = 0; i < N; i ++ ) {
if (!num.count(i)) return f[x] = i;
}
}
台阶NIM游戏
看到这题,首先想到的思路就是对前缀和进行异或,例如三级台阶从上到下各有1 1 1的石子,则对1 2 3进行异或,判断其是否为0。这看上去似乎更符合上级台阶的石子能拿到下级台阶的特性。
但仔细回顾NIM游戏的证明,必须要有两个状态稳定转换,先手一定能将状态1转化到状态2,后手只能将状态2转化成状态1。
但很显然前缀和异或是否为0这种状态定义的方式,并不符合上述稳定转化。例如三级台阶的石子分别为1 0 2,对1 1 3进行异或为3,3不为0,是状态1;但先手无论是操作成1 0 1、1 0 0、0 1 2,前缀和分别为1 1 2、1 1 1、0 1 3、异或结果都不为0,仍然是状态1,无法转化成状态2。
要想保证状态稳定,先手必须要将后手的操作抵消:
- 在NIM游戏中,如果只有两堆石子,先手是将石子拿到和另一堆一样来使得异或为0,之后后手拿多少先手也拿多少,以此抵消后手对状态的变化。
- 在台阶NIM游戏中,对于后手从台阶上拿下一级石子,先手也能仿照后手拿下一级,但这时如果还是对每级台阶都进行异或,状态仍然会改变。
如果只考虑奇数或偶数级台阶的异或,先手的操作就能抵消后手的影响。
具体来说:如果后手拿的是不考虑的台阶上的石子,先手直接将后手拿的石子继续往下一级台阶拿,要考虑的台阶石子不变,异或结果不变;如果后手拿的是要考虑的台阶上的石子,类似NIM游戏的结论2,先手仍能把要考虑的台阶上的石子拿成异或为0(例如只有两个要考虑的台阶,先手可以把另一个要考虑的台阶上的石子拿到和后手拿完一样)。
最后的问题就是要考虑奇数还是偶数的台阶?考虑总共有两级台阶的情况,当第1级台阶有任意个石子的时候,先手是必胜的,而当第1级台阶没有石子的时候,先手必败,1是奇数,因此考虑奇数的台阶,异或不等于0就是先手必胜。
综上,只考虑奇数台阶的石子异或是否为0,不为0则先手必胜。
#include <iostream>
using namespace std;
int n, a, ans;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) {
scanf("%d", &a);
if (i & 1) ans ^= a;
}
if (ans) printf("Yes\n");
else printf("No\n");
return 0;
}
拆分NIM游戏
这边题目有一些歧义,放入并不是指把石子放入场上有的石子中,而是直接摆上新的两堆石子。例如拿走一堆10个的石子,加入新的两堆9个的石子。
由于每次拆分都会令最大值减少,所以这个游戏最后能够停止。
注意这题和集合NIM游戏不同,集合NIM游戏是在同一个有向图游戏上进行的,这边拆分后会变成两个有向图游戏,所以要求这两个有向图游戏的和的SG值,需要将这两个有向图游戏的SG值异或。
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
int n, a, ans, f[105];
int dfs(int x) {
unordered_set<int> S;
if (f[x] != -1) return f[x];
for (int i = 0; i < x; i ++ ) {
for (int j = 0; j <= i; j ++ ) {
int sg = dfs(i) ^ dfs(j);
S.insert(sg);
}
}
for (int i = 0; ; i ++ ) {
if (!S.count(i)) return f[x] = i;
}
}
int main() {
scanf("%d", &n);
memset(f, -1, sizeof(f));
for (int i = 0; i < n; i ++ ) {
scanf("%d", &a);
ans ^= dfs(a);
}
if (ans) printf("Yes\n");
else printf("No\n");
return 0;
}