【博弈】叉圈棋永远都是平局
叉圈棋的规则
在一个3x3的棋盘上,先手画“ ◯ \bigcirc ◯ ”,后手画“ × \times × ”,如果某方将三个棋子连成一条直线或者斜线就获胜。
棋盘状态的表示
每一个格子都有3种状态,用
0
0
0表示格子里没有棋子,用
1
1
1表示格子里有先手画的“
◯
\bigcirc
◯ ”,用
2
2
2表示格子里有后手画的“
×
\times
× ”。这样3x3的棋盘状态共有
3
9
=
19683
3^{9}=19683
39=19683种,我们可以用一个
i
n
t
int
int类型的
3
3
3进制数来表示。
同时,从左上到右下给棋盘里的每个格子编号
0
−
8
0 - 8
0−8。
这个状态可以表示为:
s
t
a
t
u
s
=
2
∗
(
3
2
)
+
1
∗
(
3
4
)
+
1
∗
(
3
5
)
=
342
status = 2*(3^{2})+1*(3^{4})+1*(3^{5})=342
status=2∗(32)+1∗(34)+1∗(35)=342
棋盘状态的转移
如果后手在
3
3
3号位置画了一个“
×
\times
× ”,新的状态可以表示为:
s
t
a
t
u
s
’
=
s
t
a
t
u
s
+
2
∗
(
3
3
)
status’ = status + 2*(3^{3})
status’=status+2∗(33)
必胜/必败状态的确定
对于没有平局的博弈问题,我们可以将必胜状态记作
P
P
P状态,必败状态记作
N
N
N状态,两者之间的关系是:
(1) 如果当前状态的后续状态都是
P
P
P局,那么当前状态是
N
N
N状态。(无论如何都会留给对手必胜状态)
(2) 如果当前状态的后续状态存在
N
N
N局,那么当前状态是
P
P
P状态。(在最优策略下可以留给对手必败状态)
叉圈棋是带有平局的博弈问题,我们可以增加一个
D
D
D状态来表示平局状态。(这个名字是随便取的)
这里用
s
g
[
s
t
a
t
u
s
]
sg[status]
sg[status]表示
s
t
a
t
u
s
status
status的
N
N
N/
D
D
D/
P
P
P状态。
(*严格来说,这里的 s g sg sg值并不是 s g sg sg函数,只是借用了这个名字。对于某些特殊的问题,两者是等价的,但是 s g sg sg函数实际上还会考虑转移到必败态的步数)
当sg值为0时,表示当前状态是必败状态(
N
N
N)。
当sg值为1时,表示当前状态是平局状态(
D
D
D)。
当sg值为2时,表示当前状态是必胜状态(
P
P
P)。
采用的最优策略
双方都会尽可能留给对手必败状态,如果没有必败状态,也要尽可能保证平局。
也就是在后续状态的
s
g
sg
sg值中选择最小值。
s
g
[
s
t
a
t
u
s
′
]
=
2
–
m
i
n
(
s
g
[
s
t
a
t
u
s
+
t
y
p
e
∗
(
3
k
)
]
)
sg[status']=2 – min( sg[status+type*(3^{k})] )
sg[status′]=2–min(sg[status+type∗(3k)])
无法到达的状态
除此之外,受到游戏规则的约束,大部分状态是无法到达的,比如:
可以用
−
1
-1
−1来表示这些无法到达状态的
s
g
sg
sg值。
深度优先搜索(DFS)
接下来,我们使用深度优先搜索(DFS)的方法确定所有棋盘状态的 s g sg sg值。
结果发现:
(1) 无论先手第一步把“
◯
\bigcirc
◯ ”画在哪里,后手都有办法到达平局。
(2) 开局要抢占中心点和角落里的格子,如果先手画在中心则后手必须画在角上,如果先手画在角上则后手必须画在中心,然后只要封堵对手的路线上的第三个棋子即可。
(3) 必败状态是对手在两条路线上都已摆好两个棋子,无论如何都只能封堵一条路线。形成必败状态只用3个棋子就足够。
(4) 在所有状态中只有5478种状态可以到达,约占总状态数的27.8%。
最后把打好的
N
N
N/
D
D
D/
P
P
P表交给电脑,顺便写了一个叉圈棋小游戏,从此我和电脑下叉圈棋就再也没赢过。 (๑ १д१)
C/C++代码
#include<stdio.h>
#include<string.h>
const int MAXN=20000; // 棋盘状态上限
int sg[MAXN]; // sg值: 0=必败 1=平手 2=必胜 -1=无法到达的局面
int BASE3[10]; // 存放3的若干次幂
int f[10]; // 存放当前棋盘
void init(){
BASE3[0]=1;
for(int i=1;i<=9;i++) BASE3[i]=BASE3[i-1]*3;
for(int i=0;i<MAXN;i++) sg[i]=-1; // 初始化
}
int getPos(int status,int pos){
return (status/BASE3[pos])%3; // 从status状态中提取pos位置
}
int checkGameOver(int status){
// 返回值: 0=某方获胜 1=棋盘已满并且无人获胜 -1=棋盘未满并且无人获胜
int flag=1;
for(int i=0;i<9;i++){
f[i]=status%3; status/=3;
if(f[i]==0) flag=-1; // 表示棋盘没有满
}
for(int i=0;i<3;i++){
if(f[i]!=0 && f[i]==f[i+3] && f[i]==f[i+6]) return 0;
if(f[3*i]!=0 && f[3*i]==f[3*i+1] && f[3*i]==f[3*i+2]) return 0;
}
if(f[4]!=0 && f[4]==f[0] && f[4]==f[8]) return 0;
if(f[4]!=0 && f[4]==f[2] && f[4]==f[6]) return 0;
return flag;
}
int min(int a,int b){
return a<=b? a:b; // 最小值函数
}
int DFS(int status,int type){
int flag=checkGameOver(status);
if(flag!=-1){
sg[status]=flag; return flag; // 胜负已定
}
flag=3; // 后续状态的sg值一定小于3
for(int i=0;i<9;i++){
// 枚举后续状态
if(getPos(status,i)!=0) continue;
flag=min(flag,DFS(status+type*BASE3[i],3-type));
}
sg[status]=2-flag;
return sg[status];
}
int main()
{
init();
int cnt=0,res=DFS(0,1);
printf("SG[0]=%d\n",res);
for(int x=0;x<BASE3[9];x++){
if(sg[x]!=-1) cnt+=1;
}
printf("Number of Status : %d\n",cnt);
return 0;
}
(为什么C++的代码块配色这么丑呢??!)