推箱子暴力求解程序(SokobanSolver)
2017-01-17 15:39
706 查看
写这个程序是因为在看《Java并发编程实战》书的时候,提到过用多线程来解决推箱子游戏,感觉挺好玩的,于是就开始写啦!!
在这个网站你在它的规则(格式)下,也可以轻松获得推箱子地图、验证答案。
规则、格式:http://sokoban.cn/xsb_lurd.php
推箱子地图获取、答案验证:http://sokoban.cn/sokoplayer/SokoPlayer_HTML5.php
详细使用可以看下面的运行介绍
csdn:http://download.csdn.net/detail/name_z/9742995
多线程比单线程速度快,但是多线程时间不稳定,起伏较大。
其中,有多线程版本和单线程版本
程序主要分为3部分:
1.地图
2.人的移动
3.路径搜索
![](http://img.blog.csdn.net/20170116185457592?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
![](http://img.blog.csdn.net/20170116150113789?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
创建后便不可修改
![](http://img.blog.csdn.net/20170116150718046?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
![](http://img.blog.csdn.net/20170116151055184?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
保存地图及相关信息:
1.地图信息
2.人的坐标点
3.长度,宽度
4.达到当前地图信息人所走过的路径(如果解决后,最后输出的结果)
提供地图相关功能:
1.获取某坐标的字符
2.指定将某坐标的字符替换成指定字符,然后返回新的SokobanMap对象(不是返回当前对象)
检查地图是否有效,标准为:
1.地图必须为长方形的规整图形
2.不能存在无效字符(不在MapSymble没有的字符)
3.WALL必须封闭
4.必须有且仅有一个人
5.不能存在无效行(整行都是GROUND)
检查WALL是否封闭方法:
采用深度搜索,选定起始墙壁(通常为第一行中出现的第一个WALL),然后沿着墙壁遍历没走过的墙壁(只走墙壁),如果最后返回起始点则表明这墙壁是封闭的。
缺点:
不能检查出是多个闭环还是只有一个闭环
![](http://img.blog.csdn.net/20170116151754407?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
其中移动会有两种操作:
1.普通的移动,由地板到另一个地板(目标点)
2.推动箱子的移动,移动方向上有一个箱子
其中移动有可能会有两种结果,能否移动,其中,导致不能移动的原因有:
1.遇到了墙壁
2.遇到箱子,但箱子贴着墙壁
3.遇到箱子,但是箱子又贴着另一个箱子
包含两个方法:
1.当字符A移动到字符B后,字符B的坐标上应该显示什么字符
2.当字符A移动后,原来字符A的坐标上应该显示什么字符
下面列举的是方法:2.当字符A移动后,原来字符A的坐标上应该显示什么字符
![](http://img.blog.csdn.net/20170117110837503?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
1.传入初始地图,然后开始搜索
2.返回解决了的SokobanMap,如果没有则返回null
1.判断地图是否已经解决
2.判断地图是否已经走过(如果走过了,如果当前的路径比存储的路径短,则更新路径)
3.清空缓存
线程数量固定为电脑的cpu核数,每条线程负责搜索一条路径,直到该路径解决问题,当该路径已经无法继续走时,结束当前线程。在别的运行中的线程,开出一条新线程来执行新路径的搜索。
程序唤醒机制:
采用唤醒机制,来使主线程等待路径搜索线程执行完任务
多线程执行部分:
而且要让线程保持在指定的数量
这里采用了固定的线程池以及信号量来完成
然后执行中,通过信号量来限制是创建新线程来执行该路径的遍历,还是继续在当前线程中执行遍历
![](http://img.blog.csdn.net/20170116180833908?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
1.打开上面给的网址http://sokoban.cn/sokoplayer/SokoPlayer_HTML5.php
2.在红框1中选择好关卡
3.点击红框2中的输出关卡
4.在红框3中会有该关卡的地图信息
5.复制地图信息到程序中,再在下一行输入end
![](http://img.blog.csdn.net/20170116181034924?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
![](http://img.blog.csdn.net/20170116181219644?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
刚点击enter后,会显示开始时间,过一会后,会出现解决结果,如果比较复杂的图(地形大、箱子多),可能会等很久或者解不出来
![](http://img.blog.csdn.net/20170116181324738?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
![](http://img.blog.csdn.net/20170116181614524?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
1.将之前输出的结果复制到红框2中
2.再点击红框1中的载入答案,然后它就会自己运行答案
![](http://img.blog.csdn.net/20170116181754198?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
这里有些可以改烧解决这方面问题的想法:
可结合加上当前存储方法,对简单地图使用内存保存,复杂方法转向本地保存。可以增加存储的限额,当到达限额后,不再保存在内存中,而是保存到本地中。
![](http://img.blog.csdn.net/20170117143706084?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
![](http://img.blog.csdn.net/20170117150953864?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbmFtZV96/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
准备阶段
先介绍一个推箱子网站(主页):http://sokoban.cn/在这个网站你在它的规则(格式)下,也可以轻松获得推箱子地图、验证答案。
规则、格式:http://sokoban.cn/xsb_lurd.php
推箱子地图获取、答案验证:http://sokoban.cn/sokoplayer/SokoPlayer_HTML5.php
详细使用可以看下面的运行介绍
程序总结
代码
github:https://github.com/ZhongWenhui1995/SokobanSolvercsdn:http://download.csdn.net/detail/name_z/9742995
结果:
对于不复杂的图(地形不大、箱子不多或者求解比较复杂)的图,大部分可以完成任务,但对于太复杂的地图,时间很长,而且最终有可能会导致OutOfMemory异常,而解不出来。(后面有几个解决的想法)多线程比单线程速度快,但是多线程时间不稳定,起伏较大。
程序介绍
简介:
简单来说,就是使用深度遍历所有路径,直到找到解决的路径或者找不到退出。其中,有多线程版本和单线程版本
程序主要分为3部分:
1.地图
2.人的移动
3.路径搜索
类总览:
地图部分
类:
功能:
保存地图信息,以及提供关于地图的基础功能:地图检查、修改地图信息类介绍
Point:
坐标点:x,y创建后便不可修改
MapSymble:
指定地图信息用什么字符表示:墙壁、普通地板、站在目标点上的人、目标点、箱子、人、地图行间分隔符(整个地图用一个String存储时)、在目标点上的箱子MapDirection:
地图方向:上下左右SokobanMap:
创建后便不可修改保存地图及相关信息:
1.地图信息
2.人的坐标点
3.长度,宽度
4.达到当前地图信息人所走过的路径(如果解决后,最后输出的结果)
提供地图相关功能:
1.获取某坐标的字符
2.指定将某坐标的字符替换成指定字符,然后返回新的SokobanMap对象(不是返回当前对象)
MapChecker:
功能:检查地图是否有效,标准为:
1.地图必须为长方形的规整图形
2.不能存在无效字符(不在MapSymble没有的字符)
3.WALL必须封闭
4.必须有且仅有一个人
5.不能存在无效行(整行都是GROUND)
检查WALL是否封闭方法:
采用深度搜索,选定起始墙壁(通常为第一行中出现的第一个WALL),然后沿着墙壁遍历没走过的墙壁(只走墙壁),如果最后返回起始点则表明这墙壁是封闭的。
缺点:
不能检查出是多个闭环还是只有一个闭环
。。。
人的移动部分
类:
功能:
用于执行人的上下左右的移动,提供移动后的地图。其中移动会有两种操作:
1.普通的移动,由地板到另一个地板(目标点)
2.推动箱子的移动,移动方向上有一个箱子
其中移动有可能会有两种结果,能否移动,其中,导致不能移动的原因有:
1.遇到了墙壁
2.遇到箱子,但箱子贴着墙壁
3.遇到箱子,但是箱子又贴着另一个箱子
类介绍:
IMapMoveRule:
运行规则,决定一个字符移动后地图的变化(目的地点以及原地点的变化)包含两个方法:
1.当字符A移动到字符B后,字符B的坐标上应该显示什么字符
2.当字符A移动后,原来字符A的坐标上应该显示什么字符
DefaultMapMoveRule:
实现IMapMoveRule下面列举的是方法:2.当字符A移动后,原来字符A的坐标上应该显示什么字符
public Character getCharOfGoalAfterMove(char goalChar, char moveChar) {...} /** * 返回moveChar移动后原来所处的位置应该显示的字符 * @param moveChar 移动的字符(只能为人和箱子,只能为MAN,BOX,MAN_ON_GOAL,BOX_ON_GOAL) * @return 移动后原来地点应该显示的字符 * @see MapSymble */ @Override public Character getCharOfMoveAfterMove(char moveChar) { Character res = null; //如果原来是人或者箱子,则移动后就是普通的地板,如果是在目标点上的人或者箱子,则移动后就是目标点 if (moveChar == MapSymble.MAN_CHAR || moveChar == MapSymble.BOX_CHAR) { res = MapSymble.GROUND_CHAR; } else if (moveChar == MapSymble.MAN_ON_GOAL_CHAR || moveChar == MapSymble.BOX_ON_GOAL_CHAR) { res = MapSymble.GOAL_CHAR; } return res; }
ManMover:
返回人往指定方向移动一格后的地图(返回的地图是新对象),如果不可移动直接返回原来的地图。/** * 如果往该方向移动一格是合法的移动,则返回移动后的SokobanMap,否则返回原来的SokobanMap,使用默认的移动规则DefaultMapMoveRule * @param map * @param direction * @return */ public static SokobanMap moveOneStep(SokobanMap map, int direction){ return ManMover.moveOneStep(map, direction, new DefaultMapMoveRule()); } public static SokobanMap moveOneStep(SokobanMap map, int direction, IMapMoveRule mapRule) { switch (direction) { case MapDirection.UP: return ManMover.up(map, mapRule); case MapDirection.DOWN: return ManMover.down(map, mapRule); case MapDirection.LEFT: return ManMover.left(map, mapRule); case MapDirection.RIGHT: return ManMover.right(map, mapRule); default: break; } return null; }
/** * 每次移动一格,修改传入的地图参数,返回新的地图 * * @param map * @param movePoint * @param direction * @param mapRule * @return */ private static SokobanMap move(SokobanMap map, int direction, IMapMoveRule mapRule) { SokobanMap resMap = null; Point movePoint = map.manPoint; Point nextPoint = ManMover.getTargetPoint(movePoint, direction); Point nextNextPoint = ManMover.getTargetPoint(nextPoint, direction); ... //判断人的目的地点是否为地板或目标地点 else if(goalChar == MapSymble.GROUND_CHAR || goalChar == MapSymble.GOAL_CHAR){ //修改地图,字符移动后原来的坐标应该显示什么字符 resMap = map.modifyPoint(movePoint, mapRule.getCharOfMoveAfterMove(moveChar)); //修改地图,字符移动到目标坐标后,目标坐标应该显示什么字符 resMap = resMap.modifyPoint(nextPoint, mapRule.getCharOfGoalAfterMove(goalChar, moveChar)); //修改路径 resMap = new SokobanMap(resMap.getMapList(), resMap.path + MapDirection.getPath(direction)); } ... }
。。。
路径搜索部分
类:
功能:
深度搜索,直至发现解决问题的路径类介绍
ISokobanSolver:
包含一个方法:1.传入初始地图,然后开始搜索
2.返回解决了的SokobanMap,如果没有则返回null
IJudger:
包含3个方法1.判断地图是否已经解决
2.判断地图是否已经走过(如果走过了,如果当前的路径比存储的路径短,则更新路径)
3.清空缓存
DefaultJudger:
实现了IJudger,其中用Set来保存遍历过的地图信息//地图信息 private Set<String> paths = new ConcurrentSkipListSet<String>(); @Override public boolean isSolved(SokobanMap map) { if(! map.mapStr.contains(MapSymble.GOAL) && ! map.mapStr.contains(MapSymble.MAN_ON_GOAL)){ return true; } return false; } /** * 判断当前地图情况是否之前已经出现过,如果没有则将添加当前地图情况,返回false, * * @param map * @return */ @Override public boolean isPathed(SokobanMap map) { if (!paths.contains(map.mapStr)) { paths.add(map.mapStr); return false; } return true; }
ViolentSingleSolver:
单线程的暴力(深度搜索)求解程序ViolentConcurrentSolver
多线程的暴力(深度搜索)求解程序。线程数量固定为电脑的cpu核数,每条线程负责搜索一条路径,直到该路径解决问题,当该路径已经无法继续走时,结束当前线程。在别的运行中的线程,开出一条新线程来执行新路径的搜索。
程序唤醒机制:
采用唤醒机制,来使主线程等待路径搜索线程执行完任务
if (this.solvedMap == null) { //递归版本 // executeRecursiveFindPath(map); //执行路径搜索(迭代版本) executeIterateFindPath(map); //开始对lock的监听,主程序进入睡眠状态 synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }
... if (this.judger.isSolved(nextMap)) { //检测到nextMap已经解决 //保存结果地图 this.solvedMap = nextMap; //唤醒主程序 synchronized (lock) { lock.notify(); } return; } ...
多线程执行部分:
而且要让线程保持在指定的数量
这里采用了固定的线程池以及信号量来完成
private ExecutorService pool = Executors.newFixedThreadPool(POOL_SIZE); private Semaphore aliveThread = new Semaphore(POOL_SIZE);
然后执行中,通过信号量来限制是创建新线程来执行该路径的遍历,还是继续在当前线程中执行遍历
private boolean executeIterateFindPath(final SokobanMap map) { //使用信号量限制新线程的创建 if (aliveThread.tryAcquire()) { pool.execute(new Runnable() { @Override public void run() { iterateFindPath(map); //线程执行后,一定要释放信号量 aliveThread.release(); } }); return true; } return false; } private void iterateFindPath(SokobanMap map) { //采用深度搜索,因此要用栈的后进先出 Stack<SokobanMap> maps = new Stack<SokobanMap>(); maps.add(map); while (!maps.isEmpty()) { map = maps.pop(); SokobanMap nextMap = this.getNextMap(map); while (nextMap != null && this.solvedMap == null) { if (this.judger.isSolved(nextMap)) { this.solvedMap = nextMap; synchronized (lock) { lock.notify(); } return; } //如果如果线程成功,则不需要在当前线程中执行,因此不需要添加到maps中 if (!executeIterateFindPath(nextMap)) { //如果不成功则需要继续在当前线程中执行,因此要添加到maps中 maps.add(nextMap); } nextMap = this.getNextMap(map); } } }
运行介绍:
//创建求解器对象 ISokobanSolver solver = new ViolenceConcurrentSolver(new DefaultJudger()); //读取用户输入的地图信息 SokobanMap map = readMap(); //判断该地图是否有效地图 if(MapChecker.isValidMap(map)){ //开始进行求解 solver.solve(map); //获取结果 SokobanMap resMap = solver.getSolvedMap(); //判断是否解出结果 if(resMap != null){ //输出结果路径 System.out.println(resMap.path); }else{ System.out.println("can not find the way to solve the SokobanMap"); } }else{ System.out.println("this is not valid sokoban map"); }
1.获取地图,输入地图:
1.打开上面给的网址http://sokoban.cn/sokoplayer/SokoPlayer_HTML5.php
2.在红框1中选择好关卡
3.点击红框2中的输出关卡
4.在红框3中会有该关卡的地图信息
5.复制地图信息到程序中,再在下一行输入end
2.进行求解,获取结果
刚点击enter后,会显示开始时间,过一会后,会出现解决结果,如果比较复杂的图(地形大、箱子多),可能会等很久或者解不出来
3.开始验证答案
1.将之前输出的结果复制到红框2中
2.再点击红框1中的载入答案,然后它就会自己运行答案
程序优化方案(未实现)
目前代码中有一个判断地图是否已经遍历过,其中用的是set来存储是否已经走过,其中走的路径越多,存储的地图也越多,因此解决部分复杂地图的时候,就会导致OutOfMemory异常。这里有些可以改烧解决这方面问题的想法:
存储优化:
遍历过的地图信息不再保存在内存中,而是保存在本地中(数据库、文件),用空间换时间来进行优化。可结合加上当前存储方法,对简单地图使用内存保存,复杂方法转向本地保存。可以增加存储的限额,当到达限额后,不再保存在内存中,而是保存到本地中。
路径选择优化:
目前是上下左右4个方向都回进行遍历,但实际上部分路径是没有意义的路径,考虑是否通过对路径的筛选,来进行优化多线程、单线程版本时间比较
多线程版本和单线程版本采用的都是迭代的深度搜索代码:
long time = 0; final int count = 100; int successCount = 0; SokobanMap map = readMap(); for (int i = 0; i < count; i++) { ISokobanSolver solver = new ViolenceConcurrentSolver(new DefaultJudger()); // ISokobanSolver solver = new ViolentSingleSolver(new DefaultJudger()); if (MapChecker.isValidMap(map)) { try { solver.solve(map); } catch (Exception e) { e.printStackTrace(); } SokobanMap resMap = solver.getSolvedMap(); System.out.println("Time: " + solver.getSolvedTime()); if (resMap != null) { time += solver.getSolvedTime(); successCount++; System.out.println(resMap.path); } else { System.out.println("can not find the way to solve the SokobanMap"); } } else { System.out.println("this is not valid sokoban map"); } } System.out.println("Average: " + time / successCount);
样本1:
#####---- #---##### #-#-#---# #-$---$-# #..#$#$## #.@$---#- #..--###- ######---
多线程版本100次平均耗时:3700ms
单线程版本100次平均耗时:6251ms
样本2:
_#####__ _#--@### ##-#$--# #-*.-.-# #--$$-## ###-#.#_ __#---#_ __#####_
多线程版本100次平均耗时:431ms
单线程版本100次平均耗时:828ms
相关文章推荐
- 正在编写推箱子游戏的自动求解程序
- 通用逻辑问题求解程序
- 动网论坛密码暴力破解程序代码
- 孤7问题求解Prolog程序
- 如何暴力关闭一个服务程序?
- 基于并行EBE-CG方法的有限元求解程序(从我的毕业论文中节选出来的)
- PowerBuilder程序暴力破解实例(PBD文件编辑法)
- 推箱子游戏的自动求解
- 素数的求解程序
- 如何暴力关闭一个服务程序?
- 推箱子游戏的自动求解
- 数据采集服务程序--ADO连续插入记录,2天后,插入速度明显变慢,求解原因?
- 简单的蚁群算法求解TSP程序
- 射击问题求解程序-Prolog
- 递归程序求解问题的通用法则
- 再次提升“华容道自动求解”程序效率
- 一个让人发狂的PI求解C程序
- 写Hanoi 塔问题求解过程演示程序未遂
- Java游戏程序 - 推箱子
- 24点求解C程序源代码