八数码问题-A*算法-Java实现
前言:本篇博客是建立在这篇博文的基础上,是个人经过实际操作之后对其算法的改进
一、八数码问题
一个九宫格,有八个数字1-8已经确定位置,剩下一个空格以0表示,0可以和上下左右的数字交换位置。
如果给定一个初始状态1,一个目标状态2,求解从状态1到状态2最少要移动多少步
二、A*算法
1、算法定义及公式
算法是一种静态路网中求解最短路径最有效的直接搜索方法,公式表示为: f(n)=g(n)+h(n),其中:
- f(n) 是从初始状态经由状态n到目标状态的代价估计,称作估计函数
- d(n) 是在状态空间从初始状态到状态n的实际代价,称作节点深度
- h(n) 是从状态n到目标状态的最佳路径的估计代价,称作启发函数
三、八数码问题算法设计步骤
1、设计八数码节点状态结构
存储结构采取一维数组int[] num,估计函数f(n)、节点深度d(n)、启发函数h(n),以及要定义每一个状态的父状态。
这里额外设置了一个answer的线性表,用于存储最终答案路径(为什么额外设计它后面会解释)
后面的getter和setter不额外截图了
2、可达性判断函数设计(计算两个节点之间的可达性)
这里是通过计算八数码节点的逆序数判断,两个节点状态的逆序数必须同奇或者同偶才是可达状态(也就是初始状态的逆序数+目标状态的逆序数=偶数)。什么是逆序数呢?
如果一对数的前后位置与大小顺序相反,也就是如果较大的数在较小的数之前,那么就算一个逆序对,逆序数+1。逆序数是偶数的排列称作偶排列,是奇数的排列称作奇排列。如在 2,4,3,1 中,21,43,41,31是逆序,逆序数是4,是偶排列。计算八数码节点的逆序数时必须要把代表空格的0去掉,这里是原博客没有注意的地方,判断可达性的函数修改如下:
3、估计函数的设计
估计函数是由两部分构成的,节点深度d(n)其实就是当前已经走的步数,不用额外设计函数;启发函数h(n)是比较重要的一个部分,启发函数的设计直接影响了估计函数的效率,有几种定义方法:
- 当前节点与目标节点差异的度量 => 当前结点与目标节点相比,位置不符的数字个数
- 当前节点与目标节点距离的度量 => 当前结点与目标节点格局位置不符的数字移动到目标节点中对应位置的最短距离之和
- 每一对逆序数字的某倍数
- 位置不符的数字个数的总和+逆序数的三倍
这里选择的是第一种,计算当前节点与目标节点相比,有多少个数字的位置不符
4、A*算法设计
依照上面算法流程图设计就可以了。额外需要注意的是:
open表和close表使用ArrayList保存状态,open表存放所有的状态,close表则存放在搜索过程中一些较优的状态,但是close表并不是最终我们走的路径。因为我们要在搜索完成找到目标状态之后,根据父状态还原出原路径,因此原博客中的算法只能逆序输出路径。所以我额外设计了一个线性表answer就是为了在还原过程中保存我们所走的路径,从而实现正序的输出。对输出路径的函数修改:
5、其他逻辑函数设计
对子状态的f(n)的排序
根据八数码确定0在一维数组中下标的函数
判断当前状态是否在open表中的去重函数
判断移动的函数(判断是否可以上、下、左、右移动)
实现移动的函数(实现0在八数码中的上、下、左、右移动)
根据一维数组输出八数码(3*3)格式的函数
6、源码
import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Scanner; @SuppressWarnings("rawtypes") public class EightPuzzle implements Comparable{ private int[] num = new int[9]; private int evaluation; //估计函数f(n):从起始状态到目标的最小估计值 private int depth; //d(n):当前的深度,即走到当前状态的步骤 private int misposition; //启发函数 h(n):到目标的最小估计(记录和目标状态有多少个数不同) private EightPuzzle parent; //当前状态的父状态 private ArrayList<EightPuzzle> answer = new ArrayList<EightPuzzle>(); //保存最终路径 public int[] getNum() { return num; } public void setNum(int[] num) { this.num = num; } public int getDepth() { return depth; } public void setDepth(int depth) { this.depth = depth; } public int getEvaluation() { return evaluation; } public void setEvaluation(int evaluation) { this.evaluation = evaluation; } public int getMisposition() { return misposition; } public void setMisposition(int misposition) { this.misposition = misposition; } public EightPuzzle getParent() { return parent; } public void setParent(EightPuzzle parent) { this.parent = parent; } /** * 判断当前状态是否为目标状态 * @param target * @return */ public boolean isTarget(EightPuzzle target){ return Arrays.equals(getNum(), target.getNum()); } /** * 求估计函数f(n) = g(n)+h(n); * 初始化状态信息 * @param target */ public void init(EightPuzzle target){ int temp = 0; for(int i=0;i<9;i++){ if(num[i]!=target.getNum()[i]) temp++; //记录当前节点与目标节点差异的度量 } this.setMisposition(temp); if(this.getParent()==null){ this.setDepth(0); //初始化步数(深度) }else{ this.depth = this.parent.getDepth()+1;//记录步数 } this.setEvaluation(this.getDepth()+this.getMisposition());//返回当前状态的估计值 } /** * 求逆序值并判断是否有解,逆序值同奇或者同偶才有解 * @param target * @return 有解:true 无解:false */ public boolean isSolvable(EightPuzzle target){ int reverse = 0; for(int i=0;i<9;i++){ for(int j=0;j<i;j++){//遇到0跳过 if(num[j]>num[i] && num[j]!=0 && num[i]!= 0) reverse++; if(target.getNum()[j]>target.getNum()[i] && target.getNum()[j]!=0 && target.getNum()[i]!=0) reverse++; } } if(reverse % 2 == 0) return true; return false; } /** * 对每个子状态的f(n)进行由小到大排序 * */ @Override public int compareTo(Object o) { EightPuzzle c = (EightPuzzle) o; return this.evaluation-c.getEvaluation();//默认排序为f(n)由小到大排序 } /** * @return 返回0在八数码中的位置 */ public int getZeroPosition(){ int position = -1; for(int i=0;i<9;i++){ if(this.num[i] == 0){ position = i; } } return position; } /** * 去重,当前状态不重复返回-1 * @param open 状态集合 * @return 判断当前状态是否存在于open表中 */ public int isContains(ArrayList<EightPuzzle> open){ for(int i=0; i<open.size(); i++){ if(Arrays.equals(open.get(i).getNum(), getNum())){ return i; } } return -1; } /** * 一维数组 * @return 小于3(第一行)的不能上移返回false */ public boolean isMoveUp() { int position = getZeroPosition(); if(position<=2){ return false; } return true; } /** * * @return 大于6(第三行)返回false */ public boolean isMoveDown() { int position = getZeroPosition(); if(position>=6){ return false; } return true; } /** * * @return 0,3,6(第一列)返回false */ public boolean isMoveLeft() { int position = getZeroPosition(); if(position%3 == 0){ return false; } return true; } /** * * @return 2,5,8(第三列)不能右移返回false */ public boolean isMoveRight() { int position = getZeroPosition(); if((position)%3 == 2){ return false; } return true; } /** * * @param move 0:上,1:下,2:左,3:右 * @return 返回移动后的状态 */ public EightPuzzle moveUp(int move){ EightPuzzle temp = new EightPuzzle(); int[] tempnum = (int[])num.clone(); temp.setNum(tempnum); int position = getZeroPosition(); //0的位置 int p=0; //与0换位置的位置 switch(move){ case 0: p = position-3; temp.getNum()[position] = num[p]; break; case 1: p = position+3; temp.getNum()[position] = num[p]; break; case 2: p = position-1; temp.getNum()[position] = num[p]; break; case 3: p = position+1; temp.getNum()[position] = num[p]; break; } temp.getNum()[p] = 0; return temp; } /** * 按照3*3格式输出 */ public void print(){ for(int i=0;i<9;i++){ if(i%3 == 2){ System.out.println(this.num[i]); }else{ System.out.print(this.num[i]+" "); } } } /** * 将最终答案路径保存下来并输出 */ public void printRoute(){ EightPuzzle temp = null; int count = 0; temp = this; System.out.println("----------开始移动----------"); while(temp!=null){ answer.add(temp); temp = temp.getParent(); count++; } for(int i=answer.size()-1 ; i>=0 ; i--){ answer.get(i).print(); System.out.println("--------------------"); } System.out.println("最小移动步数:"+(count-1)); } /** * * @param open open表 * @param close close表 * @param parent 父状态 * @param target 目标状态 */ public void operation(ArrayList<EightPuzzle> open,ArrayList<EightPuzzle> close,EightPuzzle parent,EightPuzzle target){ if(this.isContains(close) == -1){//如果不在close表中 int position = this.isContains(open);//获取在open表中的位置 if(position == -1){//如果也不在open表中 this.parent = parent;//指明它的父状态 this.init(target);//计算它的估计值 open.add(this);//把它添加进open表 }else{//如果它在open表中 if(this.getDepth() < open.get(position).getDepth()){//跟已存在的状态作比较,如果它的步数较少则是较优解 open.remove(position);//把已经存在的相同状态替换掉 this.parent = parent; this.init(target); open.add(this); } } } } @SuppressWarnings("unchecked") public static void main(String args[]){ //定义open表 ArrayList<EightPuzzle> open = new ArrayList<EightPuzzle>(); ArrayList<EightPuzzle> close = new ArrayList<EightPuzzle>(); EightPuzzle start = new EightPuzzle(); EightPuzzle target = new EightPuzzle(); // int stnum[] = {8,6,7,2,5,4,3,0,1}; // int tanum[] = {6,4,7,8,5,0,3,2,1}; Scanner s = new Scanner(System.in); int stnum[] = new int[9]; int tanum[] = new int[9]; System.out.println("请输入初始状态:"); for(int i = 0; i< 9; i++){ stnum[i] = s.nextInt(); } System.out.println("请输入目标状态:"); for(int j= 0; j< 9; j++){ tanum[j] = s.nextInt(); } s.close(); start.setNum(stnum); target.setNum(tanum); long startTime=System.currentTimeMillis(); if(start.isSolvable(target)){ //初始化初始状态 start.init(target); open.add(start); while(open.isEmpty() == false){ Collections.sort(open); //按照evaluation的值排序 EightPuzzle best = open.get(0); //从open表中取出最小估值的状态并移出open表 open.remove(0); close.add(best); if(best.isTarget(target)){ //输出 best.printRoute(); long end=System.currentTimeMillis(); System.out.println("程序运行 "+ (end-startTime) +" ms"); System.exit(0); } int move; //由best状态进行扩展并加入到open表中 //0的位置上移之后状态不在close和open中设定best为其父状态,并初始化f(n)估值函数 if(best.isMoveUp()){//可以上移的话 move = 0;//上移标记 EightPuzzle up = best.moveUp(move);//best的一个子状态 up.operation(open, close, best, target); } //0的位置下移之后状态不在close和open中设定best为其父状态,并初始化f(n)估值函数 if(best.isMoveDown()){ move = 1; EightPuzzle down = best.moveUp(move); down.operation(open, close, best, target); } //0的位置左移之后状态不在close和open中设定best为其父状态,并初始化f(n)估值函数 if(best.isMoveLeft()){ move = 2; EightPuzzle left = best.moveUp(move); left.operation(open, close, best, target); } //0的位置右移之后状态不在close和open中设定best为其父状态,并初始化f(n)估值函数 if(best.isMoveRight()){ move = 3; EightPuzzle right = best.moveUp(move); right.operation(open, close, best, target); } } }else System.out.println("目标状态不可达"); } }