【博弈】叉圈棋永远都是平局


叉圈棋的规则

在一个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 08
图1
这个状态可以表示为: 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]=2min(sg[status+type(3k)])

无法到达的状态

除此之外,受到游戏规则的约束,大部分状态是无法到达的,比如:
图2
可以用 − 1 -1 1来表示这些无法到达状态的 s g sg sg值。


深度优先搜索(DFS)

接下来,我们使用深度优先搜索(DFS)的方法确定所有棋盘状态的 s g sg sg值。

结果发现:
(1) 无论先手第一步把“ ◯ \bigcirc ”画在哪里,后手都有办法到达平局。
(2) 开局要抢占中心点和角落里的格子,如果先手画在中心则后手必须画在角上,如果先手画在角上则后手必须画在中心,然后只要封堵对手的路线上的第三个棋子即可。
(3) 必败状态是对手在两条路线上都已摆好两个棋子,无论如何都只能封堵一条路线。形成必败状态只用3个棋子就足够。
(4) 在所有状态中只有5478种状态可以到达,约占总状态数的27.8%。

图3
最后把打好的 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++的代码块配色这么丑呢??!)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值