参考自算法竞赛进阶指南
公平组合游戏与有向无环图
一个博弈游戏被称为公平组合游戏(ICG)当且仅当其同时满足3个条件
- 由两名玩家交替行动
- 游戏任意时刻可进行的操作与轮到哪名玩家无关
- 不能进行操作的玩家判负
任何一个公平组合游戏都可以用一个有向无环图游戏模型表示
这个有向无环图有唯一的起点,表示游戏初始局面
图中每个结点表示一个局面,有向边表示能从一个局面到达另一个局面
想象初始时起点有一颗棋子,两名玩家轮流沿有向边将棋子移动一步,若某名玩家操作时棋子所在结点已无出边则判负
即分别代表了ICG中玩家交替行动和不能进行操作的玩家判负
SG函数
在有向无环图游戏模型中,对于每个节点
x
x
x,设其后继节点为
y
1
,
y
2
,
.
.
.
,
y
k
y_1,y_2,...,y_k
y1,y2,...,yk
定义
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))
其中
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}
即
m
e
x
(
S
)
mex(S)
mex(S)等于不属于集合S的最小非负整数
规定起点为s的有向图游戏G有 S G ( G ) = S G ( s ) SG(G)=SG(s) SG(G)=SG(s)
设
G
1
,
G
2
,
.
.
.
,
G
k
G_1,G_2,...,G_k
G1,G2,...,Gk为k个有向图游戏
若有向图游戏
G
G
G的操作规则为任选某个有向图游戏
G
i
G_i
Gi并在
G
i
G_i
Gi上行动一次
则
G
G
G被称为
G
1
,
G
2
,
.
.
.
,
G
k
G_1,G_2,...,G_k
G1,G2,...,Gk的和,有
S
G
(
G
)
=
S
G
(
G
1
)
x
o
r
S
G
(
G
2
)
x
o
r
.
.
.
x
o
r
S
G
(
G
n
)
SG(G)=SG(G_1)\ xor\ SG(G_2)\ xor\ ...\ xor\ SG(G_n)
SG(G)=SG(G1) xor SG(G2) xor ... xor SG(Gn)
ICG与SG函数的关系
规定有向图游戏中没有出边的结点 x x x有 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必负,当且仅当
S
G
(
x
)
=
0
SG(x)=0
SG(x)=0
一个简单的证明如下:
若结点x的某后继节点y有SG(y)=0,则mex运算后一定有SG(x)>0
即处于局面x的玩家只要向局面y移动,则一定能必胜
若结点x的后继节点均有SG(y)>0,则mex运算后一定有SG(x)=0
即处于局面x的玩家无论如何操作都一定会到达必败局面
SG函数的应用
HDU - 1847 Good Luck in CET-4 Everybody!
题目大意:初始有n张卡,两人轮流取走2的次幂(1,2,4,8…)张牌,不能取的人判负,两人均采取最优策略,求先手是否必胜
题目分析:可以直接套用上述SG函数公式,
S
G
(
n
)
=
m
e
x
(
{
S
G
(
n
−
k
)
∣
k
≤
n
A
n
d
k
=
1
,
2
,
4
,
8
,
16
,
.
.
.
}
)
SG(n)=mex(\{SG(n-k)\ |\ k \leq n\ And\ k=1,2,4,8,16,...\})
SG(n)=mex({SG(n−k) ∣ k≤n And k=1,2,4,8,16,...}),边界为
S
G
(
0
)
=
0
SG(0)=0
SG(0)=0
#include<iostream>
#include<cstdlib>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return x*f;
}
int SG[1010];
int getSG(int n)
{
if(SG[n]!=-1) return SG[n];
int check[1010];
memset(check,0,sizeof(check));
for(int k=1;k<=n;k<<=1)
check[getSG(n-k)]=1;
for(int i=0;i<1010;++i)
if(!check[i]){
return SG[n]=i;
}
}
int main()
{
memset(SG, -1, sizeof(SG));
SG[0]=0;
int n;
while(scanf("%d",&n)!=EOF)
{
if(getSG(n)==0) printf("Cici\n");
else printf("Kiki\n");
}
return 0;
}
HDU - 1848 Fibonacci again and again
题目大意:有三堆石子,分别有n、m、p个石子,两人轮流任选一个石堆取斐波那契数个石子,先取完者胜,求先手是否必胜
题目分析:
假设只有一对石子,那么思路和上题一致
S
G
(
n
)
=
m
e
x
(
{
S
G
(
n
−
f
i
b
o
[
i
]
)
∣
f
i
b
o
[
i
]
≤
n
}
)
SG(n)=mex(\{SG(n-fibo[i])\ |\ fibo[i] \leq n\})
SG(n)=mex({SG(n−fibo[i]) ∣ fibo[i]≤n}),边界为
S
G
(
0
)
=
0
SG(0)=0
SG(0)=0
当有三堆石子时,整个游戏就是三个取石子游戏的和,即
a
n
s
=
S
G
[
n
]
x
o
r
S
G
(
m
)
x
o
r
S
G
(
p
)
ans=SG[n]\ xor\ SG(m)\ xor\ SG(p)
ans=SG[n] xor SG(m) xor SG(p)
HDU - 3980 Paint Chain
题目大意:有一根含有n个珠子的环,初始时珠子都无颜色,两人轮流给连续的m个无色珠子上色,无法上色者判负,两人均采取最优策略,求先手是否必胜
题目分析:
注意到无论第一次上色怎么选,都会把剩下可上色的部分变成一个n-m的首尾不相交的链
这样问题可以转化成给n-m的链上色的游戏
一个长度为n的链上色游戏可以转化成 i 和 n-m-i 两个链上色游戏的和
即有
S
G
(
n
)
=
m
e
x
(
{
S
G
(
i
)
x
o
r
S
G
(
n
−
m
−
i
)
∣
0
≤
i
≤
n
−
m
}
)
SG(n)=mex(\{SG(i)\ xor\ SG(n-m-i)\ |\ 0\leq i \leq n-m\})
SG(n)=mex({SG(i) xor SG(n−m−i) ∣ 0≤i≤n−m})
边界条件为
S
G
(
n
)
=
0
(
n
<
m
)
SG(n)=0\ (n<m)
SG(n)=0 (n<m)
#include<iostream>
#include<cstdlib>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return x*f;
}
int SG[2010];
int getSG(int n,int m)
{
if(SG[n]!=-1) return SG[n];
if(n<m) return SG[n]=0;
if(n==m) return SG[n]=1;
int check[5010];
memset(check, 0, sizeof(check));
check[getSG(n-m, m)]=1;
for(int i=1;i<=n-m-1;++i){
check[getSG(i,m)^getSG(n-m-i,m)]=1;
}
int mex=0;
for(int i=0;i<5010;++i)
if(!check[i]){
mex=i;
break;
}
return SG[n]=mex;
}
int main()
{
int T=read();
for(int cs=1;cs<=T;++cs)
{
memset(SG, -1, sizeof(SG));
int n=read(),m=read();
if(n<m){
printf("Case #%d: abcdxyzk\n", cs);
continue;
}
else if(n==m){
printf("Case #%d: aekdycoin\n", cs);
continue;
}
if(getSG(n-m,m)==0) printf("Case #%d: aekdycoin\n", cs);
else printf("Case #%d: abcdxyzk\n", cs);
}
return 0;
}
POJ2311 Cutting Game
题目大意:有一张 w * h 的网格纸,每次操作可选择任意一张网格纸,沿格线水平或垂直将纸剪成两张,先剪出 1 * 1 的玩家获胜,在两名玩家均采取最优策略的情况下,先手是否必胜?
题目分析:
首先将该游戏转化为有向图游戏
显然一张
n
∗
m
n*m
n∗m网格纸的游戏可以分解为两个
i
∗
m
i * m
i∗m 网格纸和
(
n
−
i
)
∗
m
(n-i)*m
(n−i)∗m 网格纸的新游戏的和
所以
S
G
(
n
,
m
)
=
m
e
x
(
{
S
G
(
i
,
m
)
x
o
r
S
G
(
n
−
i
,
m
)
∣
2
≤
i
≤
n
−
2
}
⋃
{
S
G
(
n
,
i
)
x
o
r
S
G
(
n
,
m
−
i
)
∣
2
≤
i
≤
m
−
2
}
)
SG(n,m)=mex(\{SG(i,m)\ xor\ SG(n-i,m)|2\leq i \leq n-2\} \bigcup \{SG(n,i)\ xor\ SG(n,m-i)|2\leq i \leq m-2\})
SG(n,m)=mex({SG(i,m) xor SG(n−i,m)∣2≤i≤n−2}⋃{SG(n,i) xor SG(n,m−i)∣2≤i≤m−2})
边界条件
S
G
(
2
,
2
)
=
S
G
(
2
,
3
)
=
S
G
(
3
,
2
)
=
0
SG(2,2)=SG(2,3)=SG(3,2)=0
SG(2,2)=SG(2,3)=SG(3,2)=0
因为2x2,2x3,3x2的网格纸无论怎么剪都会产生1*x的纸,使得对方一定可以剪出1 * 1
#include<iostream>
#include<cstdlib>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return x*f;
}
int SG[210][210];
int getSG(int n, int m)
{
if(SG[n][m]!=-1) return SG[n][m];
int check[250];
memset(check, 0, sizeof(check));
for(int i=2;i<=n-i;++i)
{
int sg=getSG(i,m)^getSG(n-i,m);
check[sg]=1;
}
for(int i=2;i<=m-i;++i)
{
int sg=getSG(n,i)^getSG(n,m-i);
check[sg]=1;
}
int mex=0;
for(int i=0;i<250;++i)
if(!check[i]){
mex = i;
break;
}
return SG[n][m]=mex;
}
int main()
{
memset(SG, -1, sizeof(SG));
SG[2][2]=SG[2][3]=SG[3][2]=0;
int w,h;
while(scanf("%d%d",&w,&h)!=EOF)
{
if(getSG(w,h)==0) printf("LOSE\n");
else printf("WIN\n");
}
return 0;
}