0. 0. 0.写在前面
爷青回系列了属于是,因为比赛博弈论连着被打烂三天,想找点博弈论题做,做完洛谷题单之后想到了还有这本书的题单可以做哈哈哈,顺手更新一下这个题单,确实好久没更新了。
1. 1. 1.随便讲讲
1.0 随便说说
第一次接触博弈论应该是在小学奥数,小学奥数真的什么题都有,好有意思,老师会在课间在黑板上画几个模型找同学上来玩,赢了有奖励哈哈哈,最后会介绍最优解的策略。
A C M ACM ACM中的博弈论也会有像找规律的简单博弈论题目,但是多数难一点的题目还是要应用到博弈论知识,比如 S G SG SG定理,最近几天比赛疯狂被 S G SG SG定理薄纱,所以才有了这篇博客。
1.1 公平组合游戏 ( I C G ) (ICG) (ICG):
- 两名选手,交替进行预先规定好的操作
- 在任何情况下,合法操作只取决于情况本身,与选手无关
- 游戏失败的最终判定往往是选手无法进行合法操作了
对于算法竞赛中出现的博弈论题目,大多都是这种公平组合游戏的模型。
非公平组合游戏的取胜策略一般不在算法竞赛的讨论中,如五子棋,象棋,围棋等。
1.2 简单博弈论的几个模型
1.2.1 巴什博弈
一堆有
n
n
n个物品,每次取
1
−
m
1-m
1−m个拿走,拿到最后一个物品的赢。
结论:
m
+
1
∤
n
m+1\nmid n
m+1∤n先手赢,
1
+
m
∣
n
1+m\mid n
1+m∣n后手赢。
其实很好理解,当剩余的物品数为 m + 1 m+1 m+1的倍数时,我们就和对手对着取,对手取 i i i我们取 m + 1 − i m+1-i m+1−i。所以先手先拿走物品数除以 m + 1 m+1 m+1的余数,然后跟对手对着取,这就是先手获胜的策略。若开局物品数就是 1 + m 1+m 1+m的倍数,那会被对手对着取,所以后手获胜。
1.2.2 斐波那契博弈
一堆有
n
n
n个物品,第一次随便取,但不能取完,以后每次取的物品数不能超过上次的两倍,取了最后一个的获胜。
结论:
n
n
n为斐波那契数后手赢,否则先手赢。
1.2.3 威佐夫博弈
有两堆物品分别有
n
,
m
n,m
n,m个,两个人轮流从任意一堆中至少取出一个或者从两堆中取出同样多的物品,规定每次至少取一个,至多不限,最后取光者胜。
结论:
(
n
−
m
)
×
(n-m)\times
(n−m)×黄金分割比
=
m
=m
=m后手赢,否则先手赢
(
(
(不妨设
n
>
m
)
n>m)
n>m)。
1.3 I C G ICG ICG游戏模型
1.3.1 有向图游戏
给定一个有向无环图和一个起始顶点上的棋子,两名选手交替地将这枚棋子沿着有向边进行移动,最后无法移动的失败。
对于一个 I C G ICG ICG游戏,可以把每个局面看作顶点,对该局面和它的后继局面连一条有向边进行构建一个有向图,我们把它叫做博弈图。
我们通过把一个 I C G ICG ICG游戏转化为多个上述的有向图游戏的组合,再通过寻找这个游戏的一般解法,来处理 I C G ICG ICG问题。
1.3.2 性质
我们很容易可以得出下面三条性质:
1
1
1:没有后继状态的状态是必败状态。
2
2
2:一个状态是必胜状态当且仅当存在至少一个必败状态为它的后继状态。
3
3
3:一个状态是必败状态当且仅当它的所有后继状态均为必胜状态。
1.4 S G SG SG函数
1.4.1 定义
S G SG SG函数表示当前位置的一个胜败情况, S G ( x ) > 0 SG(x)>0 SG(x)>0代表在 x x x状态开始有向图游戏先手必胜,反之 S G ( x ) < 0 SG(x)<0 SG(x)<0代表 x x x状态先手必败。
1.4.2 计算
定义
m
e
x
mex
mex运算:
m
e
x
{
S
}
=
m
i
n
{
x
}
,
x
∉
S
,
x
∈
N
mex\{S\}=min\{x\},x\notin S,x\in \mathbb{N}
mex{S}=min{x},x∈/S,x∈N
对于状态
x
x
x,假设其后继状态为
y
1
,
y
2
,
.
.
.
,
y
n
y_1,y_2,...,y_n
y1,y2,...,yn,
则
S
G
(
x
)
=
m
e
x
{
S
G
(
y
1
)
,
S
G
(
y
2
)
,
.
.
.
,
S
G
(
y
n
)
}
SG(x)=mex\{SG(y_1),SG(y_2),...,SG(y_n)\}
SG(x)=mex{SG(y1),SG(y2),...,SG(yn)}。
1.4.3 理解
如何理解呢,当
S
G
(
x
)
=
0
SG(x)=0
SG(x)=0,代表对于任意
i
,
S
G
(
y
i
)
>
0
i,SG(y_i)>0
i,SG(yi)>0,对应上述结论
3
3
3,即必败态的后继均为必胜态。
当
S
G
(
x
)
≥
1
SG(x)\ge 1
SG(x)≥1,代表存在
i
,
S
G
(
y
i
)
=
0
i,SG(y_i)=0
i,SG(yi)=0,对应上述结论
2
2
2,即必胜态的后继中有必败态。
因此我们就求得了
S
G
SG
SG函数。
1.4.4 意义
S
G
(
x
)
=
0
SG(x)=0
SG(x)=0代表
x
x
x状态为
0
0
0级胜态,即为必败态
S
G
(
x
)
=
n
SG(x)=n
SG(x)=n代表
x
x
x状态为
n
n
n级胜态,即为必胜态
对于胜态的一个小性质:
一个胜态可以转化为任何一个比他级数小的胜态(包括
0
0
0级胜态,即必败态)。
上述的解释很简单,因为我们是用
m
e
x
mex
mex对
S
G
SG
SG函数进行求解,如果
S
G
(
x
)
=
n
SG(x)=n
SG(x)=n则
x
x
x的后继状态中一定包含
0
,
1
,
2
,
.
.
.
,
n
−
1
0,1,2,...,n-1
0,1,2,...,n−1级胜态,因此可以转移到任何一个比他级数小的胜态
1.5 S G SG SG定理
1.5.1 定理内容
对于一个由 n n n个有向图游戏组成的 I C G ICG ICG游戏,设他们的起点分别为 x 1 , x 2 , . . . , x n x_1,x_2,...,x_n x1,x2,...,xn,则当且仅当 S G ( x 1 ) ⊕ S G ( x 2 ) ⊕ . . . ⊕ S G ( x n ) ≠ 0 SG(x_1)\oplus SG(x_2)\oplus ...\oplus SG(x_n)\neq0 SG(x1)⊕SG(x2)⊕...⊕SG(xn)=0时先手必胜,反之先手必败。其中 ⊕ \oplus ⊕代表按位异或运算。
1.5.2 证明
结论很有意思,可用数学归纳法证明,略。(其实是我不会证明
1.5.3 应用
S G SG SG 定理适用于 任何公平的两人游戏, 它常被用于决定游戏的输赢结果。
2. 2. 2.例题
取石子游戏 1
题面
非常常见的一个板子题,看网上说这个叫巴什博弈,可以拿的数量为
1
−
m
1-m
1−m,对于对手拿
i
i
i,我们拿
1
+
m
−
i
1+m-i
1+m−i,因此先手时只需把除以
m
+
1
m+1
m+1的余数拿走即可,当总数量被
m
+
1
m+1
m+1整除时后手获胜,反之先手获胜。
#include<bits/stdc++.h>
using namespace std;
int n,k;
int main(){
cin>>n>>k;
if(n%(1+k))puts("1");
else puts("2");
}
取石子游戏 2
题面
经典的
N
i
m
Nim
Nim游戏,
s
g
i
=
n
sg_i=n
sgi=n,取所有的石子堆的异或和,若为
0
0
0则后手胜,反之先手胜。
#include<bits/stdc++.h>
using namespace std;
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,x,ans;
int main(){
read(n);
for(int i=1;i<=n;i++)read(x),ans^=x;
if(ans)puts("win");
else puts("lose");
}
移棋子游戏
题面
S
G
SG
SG定理裸题,具体可以看上面的
I
C
G
ICG
ICG有向图游戏策略,直接用定义求解每个点的
s
g
sg
sg值即可。
#include<bits/stdc++.h>
#define N 2020
#define M 6060
using namespace std;
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,m,k,cnt,ans,head[N],cd[N],rd[N],sg[N];
bool vis[N];
struct node{
int to,nxt;
}edge[M];
inline void addedge(int u, int v){
edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
void dfs(int u){
set<int> s;
for(int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(!cd[v]){
if(s.find(0)==s.end())s.insert(0);
}
else{
if(!vis[v])dfs(v),vis[v]=true;
if(s.find(sg[v])==s.end())s.insert(sg[v]);
}
}
for(int i=0;i<=n;i++){
if(s.find(i)==s.end()){
sg[u]=i;
break;
}
}
}
int main(){
read(n),read(m),read(k);
for(int i=1,u,v;i<=m;i++)read(u),read(v),addedge(u,v),cd[u]++,rd[v]++;
for(int i=1;i<=n;i++)if(!rd[i])dfs(i);
for(int i=1,x;i<=k;i++)read(x),ans^=sg[x];
if(ans)puts("win");
else puts("lose");
}
3. 3. 3.练习题
[BeiJing 2009 WC]取石子游戏
题面
因为
a
i
a_i
ai只有
1000
1000
1000,我们直接用定义暴力求出
1
−
1000
1-1000
1−1000的
s
g
sg
sg函数。
对于每一个
i
∈
[
1
,
1000
]
,
i\in[1,1000],
i∈[1,1000],遍历
j
∈
[
1
,
m
]
j\in[1,m]
j∈[1,m],则
i
i
i状态的后继状态为
i
−
b
[
j
]
i-b[j]
i−b[j],因此找到
m
e
x
{
s
g
[
i
−
b
[
j
]
]
}
mex\{sg[i-b[j]]\}
mex{sg[i−b[j]]}即为
s
g
[
i
]
sg[i]
sg[i]。
由
s
g
sg
sg定理,我们取
s
g
[
a
[
i
]
]
sg[a[i]]
sg[a[i]]的异或和,若为
0
0
0则必败
#include<bits/stdc++.h>
#define N 20
using namespace std;
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,m,x,a[N],b[N],ans,sg[1010];
set<int> s;
int main(){
read(n);
for(int i=1;i<=n;i++)read(a[i]);
read(m);
for(int i=1;i<=m;i++)read(b[i]);
for(int i=1;i<=1000;i++){
s.clear();
for(int j=1;j<=m;j++)
if(i>=b[j])s.insert(sg[i-b[j]]);
for(int j=0;j<=m;j++)
if(s.find(j)==s.end()){
sg[i]=j;
break;
}
}
for(int i=1;i<=n;i++)ans^=sg[a[i]];
if(!ans){
puts("NO");
return 0;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if((ans^sg[a[i]]^sg[a[i]-b[j]])==0){
puts("YES");
printf("%d %d\n",i,b[j]);
return 0;
}
}
}
puts("NO");
}
bzoj1299 巧克力棒
题面
题意是有若干个巧克力棒在抽屉里,每次可以吃掉任意长度的巧克力棒,也可以从抽屉里拿出来若干根巧克力棒。考虑到先手每拿一堆巧克力出来都是一个后手的
N
i
m
Nim
Nim游戏,所以先手应拿出若干个异或和为
0
0
0的巧克力棒集合,因此
d
f
s
dfs
dfs寻找是否有异或和为
0
0
0的集合,若有则先手胜
#include<bits/stdc++.h>
#define N 20
using namespace std;
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,a[N],T=10;
bool dfs(int depth, int ans, int sum){
if(depth>n){
if(!ans&&sum){
return true;
}
return false;
}
return dfs(depth+1,ans,sum)||dfs(depth+1,ans^a[depth],sum+1);
}
int main(){
while(T--){
read(n);
for(int i=1;i<=n;i++)read(a[i]);
if(dfs(1,0,0))puts("NO");
else puts("YES");
}
}
取石子
题面
首先想到操作次数为
s
=
n
−
1
+
∑
a
i
s=n-1+\sum a_i
s=n−1+∑ai,然后通过判断
s
s
s的奇偶来判断先手胜负
但是注意到如果有一堆数量为
1
1
1的,取走之后可以一次减少两个操作,因此我们分开判断
设有
s
1
s1
s1堆只有一个石子的,
s
s
s为其余石子堆的操作数。
对于
s
s
s很大的情况,我们考虑先手如何去取。
当
s
s
s为奇数时,分为两种情况:
s
1
s1
s1为奇则取一堆
1
1
1的石子和不为
1
1
1的合并;
s
1
s1
s1为偶,则从不为
1
1
1的操作数里取走一个石子。这样接下来对手做什么我们只需要做和对手相同的事情,最后即可取胜。
当
s
s
s为偶数时,分为两种情况:
s
1
s1
s1为奇则取走一堆
1
1
1的石子,后同上;
s
1
s1
s1为偶则会被对手跟着取,先手必败。
接下来考虑
s
s
s很小的情况,比如
s
=
0
,
s
=
2
s=0,s=2
s=0,s=2,此时先手取一次
s
s
s会影响操作
当
s
=
0
s=0
s=0时,我们考虑三个
1
1
1的石子分为一组,若对手取一个
1
1
1,我们便合并两堆
1
1
1,这时的操作数会一直为偶数,因此当对手没有石子可取的时候,去取那些被我们合并过的
1
1
1,我们跟着取即可。因此当
n
m
o
d
3
=
0
n\mod3=0
nmod3=0时先手必败,否则先手必胜。
s
=
2
s=2
s=2时同理。
#include<bits/stdc++.h>
#define N 20
using namespace std;
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int T,n,s1,s,x;
int main(){
read(T);
while(T--){
read(n);
s=s1=x=0;
for(int i=1;i<=n;i++){
read(x);
if(x==1)s1++;
else s+=(x+1);
}
s--;
if(s>2){
if(s&1)puts("YES");
else{
if(s1&1)puts("YES");
else puts("NO");
}
}
else{
if(s1%3)puts("YES");
else puts("NO");
}
}
}
S-Nim
题面
和练习题第一题一样,只需要暴力求出
s
g
sg
sg函数即可。
#include<bits/stdc++.h>
#define N 10020
using namespace std;
int n,m,k,s[N],sg[N];
int main(){
while(scanf("%d",&k)){
if(!k)return 0;
for(int i=1;i<=k;i++)scanf("%d",&s[i]);
memset(sg,0,sizeof sg);
set<int> st;
for(int i=1;i<=10000;i++){
st.clear();
for(int j=1;j<=k;j++)
if(i>=s[j])
if(st.find(sg[i-s[j]])==st.end()) st.insert(sg[i-s[j]]);
for(int j=0;j<=10000;j++)
if(st.find(j)==st.end()){
sg[i]=j;
break;
}
}
scanf("%d",&m);
for(int i=1,n;i<=m;i++){
int ans=0;
scanf("%d",&n);
for(int j=1,x;j<=n;j++){
scanf("%d",&x);
ans^=sg[x];
}
if(ans)putchar('W');
else putchar('L');
}
putchar('\n');
}
}
Z J O I 2009 ZJOI2009 ZJOI2009取石子游戏
题面
想了好久
s
g
sg
sg定理想不出来,原来是个区间
d
p
dp
dp。
设
L
(
i
,
j
)
,
R
(
i
,
j
)
L(i,j),R(i,j)
L(i,j),R(i,j)代表在
a
i
,
a
i
+
1
,
.
.
.
,
a
j
a_i,a_{i+1},...,a_j
ai,ai+1,...,aj的左边,右边添加
L
(
i
,
j
)
,
R
(
i
,
j
)
L(i,j),R(i,j)
L(i,j),R(i,j)个石子后变为先手必败态。
我们考虑由长度为
l
e
n
−
1
len-1
len−1的状态转移,即用
(
i
+
1
,
j
)
,
(
i
,
j
−
1
)
(i+1,j),(i,j-1)
(i+1,j),(i,j−1)来转移。
考虑利用
L
(
i
,
j
−
1
)
,
R
(
i
,
j
−
1
)
:
L(i,j-1),R(i,j-1):
L(i,j−1),R(i,j−1):
对于
a
j
=
R
(
i
,
j
−
1
)
a_j=R(i,j-1)
aj=R(i,j−1),则此时已是先手必败态,故
L
(
(
i
,
j
)
=
0
L((i,j)=0
L((i,j)=0
对于
a
j
<
R
(
i
,
j
−
1
)
a_j<R(i,j-1)
aj<R(i,j−1),分为两种情况:
如果
a
j
<
m
i
n
{
L
(
i
,
j
−
1
)
,
R
(
i
,
j
−
1
)
}
,
a_j<min\{L(i,j-1),R(i,j-1)\},
aj<min{L(i,j−1),R(i,j−1)},我们取
L
(
i
,
j
)
=
a
j
L(i,j)=a_j
L(i,j)=aj,先手取哪堆我们跟着取对称的那堆,直到取完一堆,由
L
(
i
,
j
−
1
)
,
R
(
i
,
j
−
1
)
L(i,j-1),R(i,j-1)
L(i,j−1),R(i,j−1)的定义,最后剩下的堆数一定小于
a
j
a_j
aj小于
m
i
n
{
L
(
i
,
j
−
1
)
,
R
(
i
,
j
−
1
)
}
min\{L(i,j-1),R(i,j-1)\}
min{L(i,j−1),R(i,j−1)},故我们获胜。
如果
a
j
≥
L
(
i
,
j
−
1
)
,
a_j\ge L(i,j-1),
aj≥L(i,j−1),即
L
(
i
,
j
−
1
)
≤
a
j
<
R
(
i
,
j
−
1
)
L(i,j-1)\le a_j<R(i,j-1)
L(i,j−1)≤aj<R(i,j−1),我们取
L
(
i
,
j
)
=
a
j
+
1
L(i,j)=a_j+1
L(i,j)=aj+1,对手怎么取我们都保证两侧的数相差
1
1
1,直到回到上面的情况
对于
a
j
>
R
(
i
,
j
−
1
)
a_j>R(i,j-1)
aj>R(i,j−1),同样分为两种情况:
如果
a
j
>
m
a
x
{
L
(
i
,
j
−
1
)
,
R
(
j
−
1
)
}
a_j>max\{L(i,j-1),R(j-1)\}
aj>max{L(i,j−1),R(j−1)}同上
<
m
i
n
<min
<min,取
L
(
i
,
j
)
=
a
j
L(i,j)=a_j
L(i,j)=aj
对于
a
j
≤
L
(
i
,
j
−
1
)
,
a_j\le L(i,j-1),
aj≤L(i,j−1),即
R
(
i
,
j
−
1
)
≤
a
j
≤
L
(
i
,
j
−
1
)
R(i,j-1)\le a_j\le L(i,j-1)
R(i,j−1)≤aj≤L(i,j−1),则取
L
(
i
,
j
)
=
a
j
−
1
L(i,j)=a_j-1
L(i,j)=aj−1,同上保持两侧的数相差
1
1
1,直到回到上面的情况。
所以我们完成了对 L ( i , j ) L(i,j) L(i,j)的更新,对于 R ( i , j ) R(i,j) R(i,j)则利用 L ( i + 1 , j ) L(i+1,j) L(i+1,j)更新即可。
#include<bits/stdc++.h>
#define N 1010
using namespace std;
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int T,n,a[N],le[N][N],ri[N][N];
int main(){
read(T);
while(T--){
read(n);
for(int i=1;i<=n;i++)read(a[i]),le[i][i]=ri[i][i]=a[i];
for(int len=1;len<n;len++){
for(int i=1;i+len<=n;i++){
int j=i+len,now=a[j],L=le[i][j-1],R=ri[i][j-1];
if(now==R)le[i][j]=0;
else if(now<R){
if(now<L)le[i][j]=now;
else le[i][j]=now+1;
}
else if(now>R){
if(now<=L)le[i][j]=now-1;
else le[i][j]=now;
}
now=a[i],L=le[i+1][j],R=ri[i+1][j];
if(now==L)le[i][j]=0;
else if(now<L){
if(now<R)ri[i][j]=now;
else ri[i][j]=now+1;
}
else if(now>L){
if(now<=R)ri[i][j]=now-1;
else ri[i][j]=now;
}
}
}
if(a[1]==le[2][n])puts("0");
else puts("1");
}
}