2007年3月15日星期四

推箱子的求解

  经过两周的折腾,白了头发无数,终于能算一些简单的地图了。15x15以内的地图基本上不超过5秒钟能有近似最优解。超过20x20的地图,10分钟之内没有结果,我也没耐心等下去。
  整个算法看起来是这样的:首先选择可以移动的箱子的集合,选择一个深度优先遍历,当发现下一步有重复时,选择较短的路径;当树的深度超过一个限度时,把叶子节点送入一个链表;只要那个链表不空,那么设定新的最大深度,回到第一步。为了判定初始的深度,需要对箱子与目标点作一次匹配(我选择的是最大代价匹配)。
  为了实现A*算法,使用了许多类来作支撑,比如:
   clsShortestPath      专门求迷宫的最短路径,翻译自高中时候的程序
   clsGraph_MostMatching   二部图最大匹配
   clsMapData        跟地图有关的操作在这里
   clsMapStatus       跟地图状态有关的操作在这里
   clsSolution        求解的主程序,根据一个基本clsSolutionNode作为树根,展开所有解
   clsSolutionNode      解的内容、保存下一个解的链状结构,当树的深度超过给定值时会调用制定的函数保存状态
   clsSolutionNodeHistory  纪录解的历史,在重复的解中选择路径比较短的解

  如果有人说这样写效率会不会很低?我认为不会,我没有使用virtual,不算真正的OO,在静态绑定的作用下,效率不会很低。
  代码总共33k,没办法把源代码贴出来。下面是程序到目前为止的5个版本的What's New,从中可以说明一些问题。

V1
● 用链表实现广度优先
● 计算下一级时,回收上一级内存

V2
● 改进的内存回收,能快速回收同一层的内存
● 阻止嵌入角落
● 修改了过程保存的方式(不再是string),减小了内存使用
● 防止重复上一次的步骤的反过程(比如上一次是向左,那么这一次不会向右),用list实现


V3
● 下定决心,把广度优先改成深度优先,用树实现,保存下一步的是list
● 新建clsSolutionNodeHistory,加强了重复路径搜索能力,杜绝重复的搜索
● 明确了地图clsMap、地图数据clsMapData的不同(前者只有地形数据),减小了内存占用和构造时间
● 修正了图形显示的小Bug(箱子、目标位置的标记不能通过单次循环完成)
● 新增了一个小的地图编辑器(VB)

V4
● 地图优化:自动判定不能放置箱子的区域(不需要再在地图编辑器中作弊了)
● 改进的clsSolutionNodeHistory:情况相同时,如果上一次失败了,那么取消现在的尝试;如果上一个记录还没有计算过,那么选择路径比较短的一次
● 进一步改进了状态的保存,clsMapData改名为clsMapStatus、clsMap改名为clsMapData
● 追加了一个看似十分垃圾的评价函数,以箱子与目标的距离作为判定依据,这样就形成了一个简化的A*算法
● 强制限定了运算深度,由于不能自动追加深度,还不能看作是IDA*算法
● 为了得到初始的运算深度,用网络流实现了二部图的最优匹配
● 为了提供最优匹配的原始数据(目前是每个箱子到每个目标的距离),编写了最短路经搜索代码,代码用的是高中时写的VB程序,算法名字不记得了,只知道空间复杂度O(n^4),时间复杂度O(n^2)

V5
● 题解程序将步数溢出的节点压入一个链表,IDA*得以实现
● 最优匹配从自小代价改为最大代价(也就是说,假定编地图的人让玩家总是把箱子推到最远的目标点,真是无语了)

8 条评论:

Unknown 说...

真是的,多花点时间设计算法。阻止嵌入死点那应该最先想到;地图和地图数据从开始就应该分开,还放到v3才分开……
你是不是故意弄那么多版本好玩啊,魔兽编辑器也是。
OTZ

[tc]天驰 说...

一些方案是遇到具体问题或者具体地图时才想到的。而且为了验证某些想法的正确性,首先做一个简化的版本来调试,似乎也合乎情理吧!

比如,我虽然从一开始就知道要判定死点,但是没有想到应该怎样实现。于是我就先在地图编辑器里面作弊,人工判定之。之后一直到V4版才终于实现这个功能。

至于地图数据,相信大部分写迷宫算法的人都是懒得分开的,因为基本上不太好分。我分开之后也遇到垃圾代码的编写工作,至今都还只是“补丁”水准,懒得修改。

说到底,我还是不知道为什么人一眼就看得出来解法的地图,涉及算法就变得非常困难。

Unknown 说...

推进去就动不了的是死点,这还要人工作弊?
当然,人脑多少神经元,运算能力根本就比电脑强太多了。

[tc]天驰 说...

“推进去就动不了的是死点”这样做太偏激了。有的时候虽然推进去动不了,但是阻挡它的箱子可以挪开的话,就又可以动了,不是吗?
■. ■
■X . A
■. ■
(如果X是一个箱子,那么A可以向左吗?)

除此之外,[tc]还考虑过这样一个极限的情况:
■■■■■■■■■■
■. . . . . . . . ■
■A ■■■■■■B ■
如果A那里有箱子的话,B那里就不可以有箱子(除非那是目标点)。有趣的是,如果A那里没有箱子的话,B那里却可以放一个箱子。

Unknown 说...

我仅指因为墙的关系推不动了的情况。
把算法的链接提供出来。

[tc]天驰 说...

即使是这样也挺复杂的,比如:O是目标
■■■
■O..
■. A
■. .
■■■
■. .
■. B
■. .
■■■
显然A可以向左,而B不行。

Unknown 说...

同时有两个正交的方向或以上被墙或已经不能动的箱子阻止的情况下,判定为不能动。

[tc]天驰 说...

呵呵,跟我一开始想的一样,可惜还是不对。比如:
■■■■■
■O■. .
■. ■. A
■. ■. .
■. ■. .
■. . . .
■. . . .
■■■■■
A可以向左吗?可以,因为背后有一个目标。但是正交方向上都只有墙。