![eb9ebe3c9317b40b108e71a9bab977f3.png](https://i-blog.csdnimg.cn/blog_migrate/5d93517d3ac72a4b7ad7ff66f60d2a90.jpeg)
1 布雷算法的应用
在扫雷游戏中,将雷均匀地分布在局面中依靠一种布雷算法。众所周知,在原始版本的Windows扫雷中,由于布雷算法的缺陷,使得其存在Board Cycle(局面循环)。而标准扫雷游戏中,要求每一个位置是否为雷都是独立随机事件,因此需要设计一种数学上可靠的布雷算法。
标准的扫雷游戏是在第一次左键弹起时开始布雷的,由于布雷的速度非常快(快于一帧),玩家一般不易感觉到游戏的停顿。但是在玩超大尺寸的扫雷时,仍然需要保证玩家感觉不到游戏的停顿,这就需要有一种快速可靠的布雷算法。
此外,在测算各局面的3BV的概率分布时,目前的主流做法仍然是通过大量的局面仿真。为得到精确可靠的3BV的概率分布,曾有玩家连续仿真超过1万亿局高级局面。在这个过程中,布雷算法的性能是否优越,极大地影响到仿真能否快速完成。
2 扫雷的布雷算法
本文介绍几种扫雷的布雷算法。为突出重点,所有算法不考虑起手不能点到雷的规则,并且所有布雷算法的输出均为一维数组,其长度为高×宽;在这个一维数组中,雷用数字-1表示,非雷用0表示。代码均采用Python编写,在代码中,Row为局面的高,默认为16;Column为局面的宽,默认为30;Num为总雷数。每个代码均执行10000次,并在普通pc上重复运行若干次,取最短用时为这种算法的用时。
2.1 算法一——朴素的算法
最朴素简单的算法就是,随机选一个位置,如果没有雷就摆一个雷,这个方法与现实中物理手段的布雷很像。具体步骤是这样的:
Step1:随机寻找一个位置
Step2:如果该位置是空的,那么则布雷
Step3:如果该位置是雷,那么返回step1
Step4:直到布雷数达到总雷数
代码如下:
y=[0]*Row*Column
Numi=0
while Numi < Num:
while 1:
idd = random.randint(0, Row*Column-1);
if y[idd] != -1 :
y[idd] = -1
Numi+=1
break
经测试,当雷数为99时,算法的运行时间为1.2754秒。但是这种算法在雷数较大时性能急剧下降,因为随机选择的位置大概率是雷。当雷数为479时,算法的运行时间长达33.0841秒。
2.2 算法二——性能恒定
算法一的性能受雷数影响非常大,算法二的性能则完全不受雷数的影响。这种算法有一点动态规划的思想,其基本思路是,依次访问一维局面数组的每一个位置,并按照采用某种方法计算出的概率,来决定该位置是否为雷。在这个过程中,需保证每一个格子为雷的综合概率正好是 总雷数/总面积。
步骤为:
Step1:从起始位置开始依次访问数组的每个位置
Step2:当前位置布雷的概率为:未布的雷数/未访问的位置数
Step3:直到遍历完整个数组
代码如下:
y=[0]*Row*Column
mineLeft=Num
for point in range(0,Row*Column):
if random.randint(1,Row*Column-point)<=mineLeft:
y[point]=-1
mineLeft-=1
经测试,当雷数为99时,算法的运行时间为5.0708秒。当雷数为479时,算法的运行时间为5.5179秒。即这种算法的性能基本不受雷数影响。
2.3 算法三——算法一的改进算法
算法三是对算法一的直接改进。既然算法一的性能的下降是由于随机选择的位置大概率是雷,那么我们只要在每次选择出布雷位置后,将这个位置从下次挑选范围内除去即可。
即维护两个列表:AllId和MineId,前者是所有不是雷的方格ID,后者是所有雷的ID。
代码如下:
y=[0]*(Row*Column)
AllId=list(range(0,Row*Column))
MineId=[]
MineLaid=0
while MineLaid<Num:
Id=random.randint(0,Row*Column-MineLaid-1)
MineId.append(AllId[Id])
AllId.pop(Id)
MineLaid+=1
for i in MineId:
y[i]=-1
经测试,当雷数为99时,算法的运行时间为1.4729秒。当雷数为240时,算法的运行时间为3.5417秒。当雷数为479时,算法的运行时间为6.8174秒。
2.4 算法四——洗牌算法
可以看到,算法一在雷数较少时比较快,而算法二在雷数较多时比较快。算法三虽然综合了这两种算法的优点,但似乎又不够优雅,尤其是维护两个列表的操作似乎比较繁琐。
那么有没有一种算法能同时超越以上三种算法呢?答案是肯定的。接下来介绍的就是大名鼎鼎的洗牌算法。
洗牌算法的步骤为:
Step1:初始化一维局面数组,使得前Row*Column-Num个位置均为0,后Num个位置均为-1
Step2:遍历数组从id= Row*Column-Num到id=Row*Column-1的位置
Step3:交换当前访问位置与在当前访问位置之前随机选取的位置的值
代码如下:
y=[0]*(Row*Column-Num)
y=y+[-1]*Num
for i in range(Row*Column-Num,Row*Column):
idd=random.randint(0,i-1)
y[idd],y[i]=y[i],y[idd]
经测试,当雷数为99时,算法的运行时间仅为1.1901秒。当雷数为479时,算法的运行时间仅为5.4258秒。可见洗牌算法在各方面均超越了前面三种算法。
2.5 算法五——改进的洗牌算法
在标准洗牌算法的基础上,我们很容易能做出一个重大的改进。
简单地说就是,当雷数少于一半时,算法不变;但当雷数多于一半时,我们把非雷看作雷,而把雷看作非雷。这样仅仅添加一个选择语句后,就兼顾了算法在雷数较多与较少时的性能。
代码如下:
if Num<Row*Column/2:
y=[0]*(Row*Column-Num)
y=y+[-1]*Num
for i in range(Row*Column-Num,Row*Column):
idd=random.randint(0,i-1)
y[idd],y[i]=y[i],y[idd]
else:
y=[-1]*Num
y=y+[0]*(Row*Column-Num)
for i in range(Num,Row*Column):
idd=random.randint(0,i-1)
y[idd],y[i]=y[i],y[idd]
经测试,当雷数为99时,算法的运行时间为1.2161秒。当雷数为240时,算法的运行时间为2.9356秒。当雷数为479时,算法的运行时间仅为0.0492秒。
2.6 算法六——极限
在算法五的基础上引入一些技巧,还以进一步提升算法的性能。
优化后的代码如下:
area=Row*Column
if Num<area/2:
y=[0]*(area-Num)
y=y+[-1]*Num
for i in range(area-Num,area):
idd=random.randint(0,i-1)
if y[idd]!=-1:
y[idd]=-1
y[i]=0
else:
y=[-1]*Num
y=y+[0]*(area-Num)
for i in range(Num,area):
idd=random.randint(0,i-1)
if y[idd]!=0:
y[idd]=0
y[i]=-1
经测试,当雷数为99时,算法的运行时间为1.1559秒。当雷数为240时,算法的运行时间为2.7441秒。当雷数为479时,算法的运行时间为0.0481秒。
3总结
其实在大部分情况下,这六种方法都是可以满足普通扫雷游戏中的要求的,只有在计算3BV的概率分布时,对布雷算法的优化研究才有价值。
扫雷游戏的布雷算法介绍到此结束,本文旨在抛砖引玉。对扫雷有兴趣的朋友可继续研究,后续问题可以在后台留言!