关于规则:
初始格局(黑黑黑白白白空),最终格局(白白白黑黑黑空),每次可以将滑块移入相邻空格,也可以跳过最多两个其他滑块进入空格。
耗散值gn的计算:移入相邻空格gn=gn+1;跳过m个其他滑块进入空格gn=gn+m。
评估值hn的计算:每个白块前黑块的数目之和,如初始格局hn为9,最终格局hn为0。
关于算法:
1. 在描述算法之前,首先阐述几个本程序中几个比较重要的“容器”。
①优先队列(PriorityQueue):即为此问题中的open表,用于存放已存在但还未扩展的节点。重写Comparator,使耗散值gn+评估值hn较小的排在队首。
②节点(Status):用于存放节点,其成员变量father执行了close表的功能。
③键值对(Map<Long, Integer>):存储“格局-gn”映射,执行isOpen表和isClose的功能(isOpen表和isClose表分别为open表和close表的判重表)。格局用一个hash值来唯一表示。
此处hash值选用的的是Zobrist值,Zobrist值的产生方法:https://blog.csdn.net/aqzwss/article/details/52850737
2. 算法描述:主要思路为“出队-扩展-判定入队还是舍弃-出队”循环
①取优先队列队首元素。
②取出空格位置empty,对于empty-3到empty+3范围内的每一格j,若j未越界且不为空格,则进行尝试移动,并生成新节点。
③对于新节点a,若在map中能查到此格局,且a.gn小于map中记录的gn,则将a入队,更新map;若a.gn大于等于map中的gn或在map中查不到该格局,则舍弃该节点(这里融合了对open表判重和close表的判重,后面有详细解释)。
④回到①,不断循环,直至取出的节点格局为目标格局位置。
关于一些细节的处理:
1. status存储使用byte[],节省内存。
2. 评估值hn的计算用一层循环替换两层循环,减小时间复杂度。
3. 键值对内存储的是<格局对应zobrist值,gn>,经过分析,此处使用zobrist值来唯一表示一个格局并进行相关操作在时间上会明显快于其他表示方式。
4. 本程序中并未使用分开的isOpen表和isClose表来对open表和close表进行分别判重,而是将isClose表和isOpen表结合成一个map,对于滑动积木块问题,此方法证明无误,而且可以省去一些操作。感兴趣的可以看下面详解(isOpen表和isClose表分别为open表和close表的判重表)。
5. 关于单调性限制的证明也会在最后附上。
6. 下面先附上代码。
public class Status {
public static byte SIZE;
private byte status[] = new byte[SIZE*2+1]; //当前格局
private int gn; //累计路径耗散值(Dissipative value)
private int hn; //评价函数(Evaluation function)
private Status father; //存放父节点位置
private int direction; //存放移动方向
private long zobrist;
private int empty;
public Status() {
}
public Status(byte[] s, int g, Status father, int direction, long zobrist){ //构造函数
if (s.length != SIZE*2+1)
System.out.p