博弈论
公平组合游戏ICG
- 若一个游戏满足:
- 由两名玩家交替行动;
- 在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关; .
- 不能行动的玩家判负;
-
则称该游戏为一个公平组合游戏。
-
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。
-
先手必胜状态:先手操作一次后可以使剩下的状态变成必败状态。
-
先手必败状态:不能做任何一次操作使得剩下的状态成为必败状态。
-
M e x Mex Mex 运算:
设S表示一个非负整数集合。定义 m e x ( S ) mex(S) mex(S) 为求出不属于集合S的最小非负整数的运算,即: m e x ( S ) = m i n { x , x 属于自然数,且 x 不属于 S } mex(S) = min\{x, x属于自然数,且x不属于S\} mex(S)=min{x,x属于自然数,且x不属于S}。 -
S G SG 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 ( S ) mex(S) mex(S) 运算的结果,即
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)})
特别地,整个有向图游戏 G G G 的 S G SG SG 函数值被定义为有向图游戏起点 S S S 的 S G SG SG 函数值,即 S G ( G ) = S G ( S ) SG(G) = SG(S) SG(G)=SG(S)。
- 有向图游戏的和:
设 G 1 , G 2 , . . . . G m G_1, G_2, .... G_m G1,G2,....Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏 G i G_i Gi,并在 G i G_i Gi 上行动一步。G被称为有向图游戏 G 1 , G 2 , . . . G n G_1, G_2, ... G_n G1,G2,...Gn
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:
S G ( G ) = S G ( G 1 ) ⊕ S G ( G 2 ) ⊕ . . . ⊕ S G ( G m ) SG(G) = SG(G1) \oplus SG(G2) \oplus ... \oplus SG(Gm) SG(G)=SG(G1)⊕SG(G2)⊕...⊕SG(Gm). - 定理:
有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。 - 证明方式和经典 Nim 游戏一模一样。注意从起点S一定可以走到小于 SG(S) 的任意一个数字,这个由 SG的定义就知道。
891. Nim游戏
- 给定n堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。
- 若一开始是N堆石子: a 1 , a 2 , a 3 , . . . , a n a_1,a_2,a_3,...,a_n a1,a2,a3,...,an
- a 1 ⊕ a 2 ⊕ a 3 ⊕ . . . ⊕ a n = 0 a_1 \oplus a_2\oplus a_3\oplus...\oplus a_n=0 a1⊕a2⊕a3⊕...⊕an=0:先手必败。
- a 1 ⊕ a 2 ⊕ a 3 ⊕ . . . ⊕ a n ! = 0 a_1 \oplus a_2\oplus a_3\oplus...\oplus a_n!=0 a1⊕a2⊕a3⊕...⊕an!=0:先手必胜。
- 证明:假设 a 1 ⊕ a 2 ⊕ . . . ⊕ a n = x a_1 \oplus a_2 \oplus ... \oplus a_n = x a1⊕a2⊕...⊕an=x,假设 x x x 的二进制表示中,最高的一位1在第k位,那么一定存在一个数 a i a_i ai,其第 k k k 位是1. 那么一定有 a i ⊕ x < a i a_i\oplus x <a_i ai⊕x<ai,因此,我们让 a i a_i ai 变成 a i ⊕ x a_i \oplus x ai⊕x,就可以让异或和为0. 方法就是从 a i a_i ai 中拿走 a i − ( a i ⊕ x ) a_i - (a_i\oplus x) ai−(ai⊕x) 个石子 ( a i − ( a i − ( a i ⊕ x ) ) = a i ⊕ x a_i - (a_i - (a_i \oplus x)) = a_i \oplus x ai−(ai−(ai⊕x))=ai⊕x).
892. 台阶-Nim游戏
-
题意:现在,有一个n级台阶的楼梯,每级台阶上都有若干个石子,其中第 i i i 级台阶上有 a i a_i ai 个石子 ( i ≥ 1 ) (i≥1) (i≥1)。两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。问如果两人都采用最优策略,先手是否必胜。
-
奇数级台阶上石子异或起来等于0,先手必败。非零先手必胜。
-
我们仿照经典 Nim 游戏的思考方式,首先全为0的时候为必败态。如果有一个 a i a_i ai不为0,那么需要移动 i i i 次才能到达0台阶。这就意味着,假设 a 1 ⊕ a 3 ⊕ a 5 . . . = x a_1 \oplus a_3 \oplus a_5 ... = x a1⊕a3⊕a5...=x,找到一个 a i a_i ai,拿走 a i − ( a i ⊕ x ) a_i - (a_i\oplus x) ai−(ai⊕x) 放到 a i − 1 a_{i-1} ai−1 中。
893. 集合-Nim游戏
-
题意:给定n堆石子以及一个由k个不同正整数构成的数字集合S。现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合S,最后无法进行操作的人视为失败。问如果两人都采用最优策略,先手是否必胜。
-
很简单的一个模拟。用记忆化搜索来防止指数级别的深搜复杂度。
#include<iostream>
#include<algorithm>
#include<unordered_set>
#include<cstring>
using namespace std;
const int maxn = 110, maxm = 10010;
int N, M;
int s[maxn], f[maxm];
int sg(int x) {
if (f[x] != -1) return f[x];
unordered_set<int> S;
for (int i = 0; i < M; i++) {
if (x - s[i] >= 0) S.insert(sg(x - s[i]));
}
for (int i = 0; ; i++) {
if (!S.count(i)) return f[x] = i;
}
}
int main() {
scanf("%d", &M);
for (int i = 0; i < M; i++) scanf("%d", &s[i]);
scanf("%d", &N);
memset(f, -1, sizeof f);
f[0] = 0;
int res = 0;
while (N--) {
int x;
scanf("%d", &x);
res ^= sg(x);
}
if (res == 0) printf("No\n");
else printf("Yes\n");
return 0;
}
894. 拆分-Nim游戏
- 题意:给定n堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。问如果两人都采用最优策略,先手是否必胜。
- 有一个性质, x x x 可以拆成 ( a 1 , a 2 ) , ( b 1 , b 2 ) . . . (a_1, a_2),(b_1, b_2)... (a1,a2),(b1,b2)...,那么 S G ( x ) = m e x ( { S G ( a 1 , a 2 ) , S G ( b 1 , b 2 ) . . . } ) = m e x ( S G ( a 1 ) ⊕ S G ( a 2 ) , S G ( b 1 ) ⊕ S G ( b 2 ) . . . ) SG(x) = mex(\{SG(a_1, a_2),SG(b_1, b_2)...\}) = mex({SG(a_1)\oplus SG(a_2), SG(b_1)\oplus SG(b_2)...}) SG(x)=mex({SG(a1,a2),SG(b1,b2)...})=mex(SG(a1)⊕SG(a2),SG(b1)⊕SG(b2)...).
#include<cstring>
#include<algorithm>
#include<iostream>
#include<unordered_set>
using namespace std;
const int maxn = 110;
int f[maxn], 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() {
scanf("%d", &N);
memset(f, -1, sizeof f);
//f[0] 不初始化似乎也可以
int res = 0;
for (int i = 0; i < N; i++) {
int x;
scanf("%d", &x);
res ^= sg(x);
}
if (res == 0) printf("No\n");
else printf("Yes\n");
return 0;
}
1319. 移棋子游戏
- 题意:给定一个有 N 个节点的有向无环图,图中某些节点上有棋子,两名玩家交替移动棋子。玩家每一步可将任意一颗棋子沿一条有向边移动到另一个点,无法移动者输掉游戏。对于给定的图和棋子初始位置,双方都会采取最优的行动,询问先手必胜还是先手必败。
- 每一个棋子都是一个独立的图,因此将所有棋子作为图的起点求其对应的 SG 值,异或起来即可。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<unordered_set>
using namespace std;
const int maxn = 2010, maxm = 6010;
int h[maxn], e[maxm], ne[maxm], idx;
int f[maxn], N, M, K;
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int sg(int x) {
if (f[x] != -1) return f[x];
unordered_set<int> S;
for (int i = h[x]; i != -1; i = ne[i]) {
int u = e[i];
S.insert(sg(u));
}
for (int i = 0;; i++) {
if (!S.count(i)) return f[x] = i;
}
}
int main() {
scanf("%d%d%d", &N, &M, &K);
memset(f, -1, sizeof f);
memset(h, -1, sizeof h);
while (M--) {
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
int res = 0;
while (K--) {
int x;
scanf("%d", &x);
res ^= sg(x);
}
if (res == 0) printf("lose\n");
else printf("win\n");
return 0;
}
1321. 取石子
-
题意:游戏开始时,有 N 堆石子排成一排,然后他们轮流操作(Alice 先手),每次操作时从下面的规则中任选一个:(1)从某堆石子中取走一个;(2)合并任意两堆石子。不能操作的人输。Alice 想知道,她是否能有必胜策略。
-
说一下为什么在简单情况下(简单状况即每一堆的石子数 > 1),b是奇数等价于先手必胜:
- 对于每一种奇数状态:
- 若堆数 > 1,合并两堆;
- 若堆数 = 1,取一个石子(此堆的石子必然是奇数,因为b是奇数)
- 对于每一种偶数状态:
- 若合并,那么b就变成奇数
- 若取石子:
- 若该堆的石子数 > 2,取走后仍是简单状态
- 若该堆的石子数 = 2,取走一个就是1,那么下一个人为了赢,若此时堆数 > 1, 就会把其中这一个石子和其他堆合并;当然如果此时堆数 = 1的话,拿走就赢了就赢了)。
- 对于每一种奇数状态:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 60, maxm = 50010;
int f[maxn][maxm];
int N;
int dp(int a, int b) {
int& v = f[a][b];
if (v != -1) return v;
if (a == 0) return v = b % 2;
//小心这个边界
if (b == 1) return dp(a + 1, 0);
//a拿走一个
if (a && !dp(a - 1, b)) return v = 1;
//b拿走一个或者b内部合并
if (b && !dp(a, b - 1)) return v = 1;
//a内部合并
if (a >= 2 && !dp(a - 2, b ? b + 3 : b + 2)) return v = 1;
//a, b合并
if (a && b && !dp(a - 1, b + 1)) return v = 1;
return v = 0;
}
int main() {
int T;
scanf("%d", &T);
memset(f, -1, sizeof f);
while (T--) {
int N;
scanf("%d", &N);
int a = 0, b = 0;
for (int i = 0; i < N; i++) {
int x;
scanf("%d", &x);
if (x == 1) a++;
else b += b ? x + 1 : x;
}
if (dp(a, b)) printf("YES\n");
else printf("NO\n");
}
return 0;
}
1322. 取石子游戏
- 题意:有 n 堆石子,将这 n 堆石子摆成一排。游戏由两个人进行,两人轮流操作,每次操作者都可以从最左或最右的一堆中取出若干颗石子,可以将那一堆全部取掉,但不能不取,不能操作的人就输了。对于任意给出的一个初始局面,是否存在先手必胜策略。
- 浙江2009年省选,挖坑。