迷。
老规矩
作业要求
设计一个迷宫游戏,给定迷宫的入口;如果存在出口,程序能够显示行走的路径,并最终到达出口,并输出“成功走出迷宫”;如果不存在出口,程序也能够显示行走的过程,并最终回退到入口,并输出“回退到入口”;
最终成果
实现思路
要是以为这个作业的难度在于走迷宫,那就大错特错了。走迷宫不要太简单,像我这样完全没接触过OI、ACM的普通学生,一般都能想到用所谓的广度优先算法解决。
实际上,这个问题困难的地方在于生成迷宫,如何生成一个有且仅有一条通路的、视觉效果还不赖的迷宫,是做这个作业最终要解决的问题。
走迷宫
既然走迷宫是很容易解决的问题,我们就先挑这个软柿子捏捏。
按意愿编程法
《SICP》提出过这么一种编程思路 —— 按意愿编程。意思是,先别管你写的程序能不能跑,假装代码中的所有函数都能正常工作,尽管实际上还没实现。这个办法能让人从细节的纠缠中脱离,从大局出发,每次专注于某个功能的实现。
当初我用Scheme来写走迷宫就采用了这样的办法,所以非常快地解决了问题。
主要函数
是该有这么一个find_way
函数,功能是这样的:查看走出迷宫没有,如果没有,那就走下一步,再查看走出迷宫没有,如果没有,那就走下一步,……,直至走出迷宫。find_shortest_way
函数就是按意愿编程法的产物,因为我还没实现它,只是期望它能够帮我查找ways
的所有路线中是否存在某条路线走出了迷宫。若找到了这条路线,find_way
就圆满完成了任务,返回这条最短路线。
接下来的for
循环,是由于我的Map
函数不够完善而临时采用的遍历操作。它利用假想的next_steps
函数,让ways
中的每条路线都走出下一步,然后把这些“下一步”存储在poor_new_ways
中。叫poor
的原因是,我假想的next_steps
函数直接产生上下左右四个新位置,还需要下一步的过滤操作。
过滤操作利用is_forwardable
,就可以筛选出真正可以走的下一步。
最后再递归进行如上操作。
|
|
下面再去实现上面所需要的子函数。
下一步
生成某条路线的所有可能的下一步。first
、second
分别代表坐标x
、y
。C
函数用于构造坐标。
|
|
可达与否
下一步不可达的情况是:走出了边界范围、撞了墙、走了别的路线走过的位置。
需要特别提出的是:走了别的路线走过的位置。感觉这是一种挺精巧的想法:既然你慢了一步,不是第一条走这个位置的路线,你就已经失去了成为最短路线的可能,淘汰吧。
|
|
到达终点与否
|
|
到此,走迷宫的代码已经完成。马上迎来重头戏:生成迷宫。
生成迷宫
思路
首先看到这样一个有望成为迷宫的胚子:
我们需要做的是,把某些墙推倒,解放每个格子(cell)。离散数学中的最小生成树算法就派上了用场。
把每个格子都当成一个结点,让所有格子连成一棵树。树的每两个结点,都必定只有一条最短路径可达。这样恰好就是一个迷宫。
既然是最小生成树,那么每个结点的权重是多少?这里假设每个格子的东南西北四个方向邻接的格子权重相同,与其他格子的距离无限大。并采用随机挑选的办法来处理权重相同的结点。
Prim 算法
Prim算法是比较简单的最小生成树算法。我把这种思路整理成了如下的伪代码:
首先明确,任何已连通的格子周围的格子都是待连通的格子。于是算法可以总结成:最开始先随机找到一个格子,加入已连通的集合。再从待连通集合里随机挑选一个格子,推倒它与已连通集合中随机挑选出的它周围的格子之间的墙。重复前过程直至待连通集合再无格子。
参考链接: http://weblog.jamisbuck.org/2011/1/10/maze-generation-prim-s-algorithm
观察我的C++代码,基本与上述的伪代码一一对应:
最后就会得到一个长这样的迷宫:
寒假作业终于告一段落,要开始写周末作业了……
完整代码:https://github.com/zhongzc/C-/blob/master/maze.cpp