前言:本篇博客是建立在這篇博文的基礎上,是個人經過實際操作之后對其算法的改進
一、八數碼問題
一個九宮格,有八個數字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("目標狀態不可達"); } }