人工智慧之八數碼問題

Wangchinlung發表於2017-05-22

八數碼難題實驗報告

問題描述

八數碼難題  

  3×3九宮棋盤,放置數碼為1 - 8的8個棋牌,剩下一個空格,只能通過棋牌向空格的移動來改變棋盤的佈局。

  • 求解的問題——給定初始佈局(即初始狀態)和目標佈局(即目標狀態),如何移動棋牌才能從初始佈局到達目標佈局

·        解答路徑——就是一個合法的走步序列

初始狀態S0:  2     3   目標狀態Sg:1  2  3 

                          1  8  4                            8 0  4                                       

                          7  6 5                             7 6  5

問題分析

採用啟發式搜尋的方式解決此問題,利用A*演算法,通過估價函式來選擇一條最佳路徑。

•   估價函式的定義:
對節點n定義f*(n)=g*(n)+h*(n),表示從S開始約束通過節點n的一條最佳路徑的代價。
希望估價函式f 定義為:f(n)=g(n)+h(n)
                            —— g是g*的估計 ,h是h*的估計

•   g*(n):從s到n的最小路徑代價值

•   h*(n):從n到g的最小路徑代價值

•   f*(n)=g*(n)+h*(n):從s經過n到g的最小路徑的總代價值

•   g(n)、h(n)、f(n)分別是g*(n)、h*(n)、f*(n)的估計值

•   g(n)通常選擇為當前所找到的從初始節點S到節點n的“最佳”路徑的代價值,顯然有g(n) ³g*(n)

具體解決思路是,通過給定的初始狀態,推算該狀態下能產生的所有可能的情況,加入到等待佇列中,通過A*演算法來計算這些狀態接近結果的距離大概是多少,然後根據這個距離來選取最小的狀態繼續搜尋,如此迴圈,直到找到結果。

下面是具體實現程式碼,首先是主流程類:

import java.util.ArrayList;
import java.util.Scanner;

/**
 * 程式入口,這裡主要是輸入初始資料
 * @author 41571
 *2 8 3 1 6 4 7 0 5
 */
public class Project {
	private int total = 0;
	public static int[][] Goal = {{1,2,3},{8,0,4},{7,6,5}};
	private Node ini;
	private ArrayList<Node> nodeList = new ArrayList<Node>();//需要檢測的情況
	private ArrayList<Node> result = new ArrayList<Node>();//檢測過的情況
	private ArrayList<Node> end = new ArrayList<Node>();//結果
	
	public Project(){	//初始化最初狀態
		System.out.println("輸入初始狀態,0表示空格");
		Scanner in = new Scanner(System.in);
		String numString = in.nextLine();
		ini = new Node(numString);
		nodeList.add(ini);
		Looking();
	}
	
	public static void main(String args[]){		//主函式
		new Project();
	}
	
	public void Looking(){	//遍歷,根據權值遍歷
		Node pro;boolean a = true;
		while(a){
			sort();		//氣泡排序,把權值最小的放在佇列最後
			pro = nodeList.get(nodeList.size() - 1);	//獲得棧頂元素
			if(isSame(pro)){	//當前要檢測的節點是否已經遍歷過了
				total++;
				result.add(pro);		//先加入檢測過的佇列
				int[][] data = pro.getNum().getData();	//獲得當前檢測的節點資料
				int deep = pro.getDepth();		//獲得當前檢測的節點的深度
				nodeList.remove(nodeList.size() - 1);	//把當前節點移出遍歷佇列
				if(pro.getNum().isSame(Goal)){		//當前節點是否是目標
					print();
					break;
				}else{		//不是目標,開始生成移動後的節點
					if(pro.getNum().isUp()){
						int[][] da = new int[3][3];
						for(int i = 0;i <3;i ++)for(int j = 0;j < 3;j ++){da[i][j] = data[i][j];}
						Num num = new Num(da);
						Node node = new Node(num.up(),deep+1,pro);
						if(isSame(node)){
							nodeList.add(node);	//生成移動後的節點並把當前節點作為父節點加入其中
							total++;
						}
					}
					if(pro.getNum().isDown()){
						int[][] da = new int[3][3];
						for(int i = 0;i <3;i ++)for(int j = 0;j < 3;j ++){da[i][j] = data[i][j];}
						Num num = new Num(da);
						Node node = new Node(num.down(),deep+1,pro);
						if(isSame(node)){
							nodeList.add(node);
							total++;
						}
					}
					if(pro.getNum().isLift()){
						int[][] da = new int[3][3];
						for(int i = 0;i <3;i ++)for(int j = 0;j < 3;j ++){da[i][j] = data[i][j];}
						Num num = new Num(da);
						Node node = new Node(num.lift(),deep+1,pro);
						if(isSame(node)){
							nodeList.add(node);
							total++;
						}
					}
					if(pro.getNum().isRight()){
						int[][] da = new int[3][3];
						for(int i = 0;i <3;i ++)for(int j = 0;j < 3;j ++){da[i][j] = data[i][j];}
						Num num = new Num(da);
						Node node = new Node(num.right(),deep+1,pro);
						if(isSame(node)){
							nodeList.add(node);
							total++;
						}
					}
				}
			}else	//已經遍歷過這個節點,直接移除
				nodeList.remove(nodeList.size() - 1);
		}
	}
	private boolean isSame(Node pro) {	//判斷pro是否遍歷過,即是否出現在遍歷過的佇列中
		// TODO Auto-generated method stub
		for(int i = 0;i < result.size();i ++){
			if(result.get(i).getNum().isSame(pro.getNum().getData())){
				return false;
			}
		}
		return true;
	}
	private void print() {		//輸出結果函式
		// TODO Auto-generated method stub
		delEnd(result.get(result.size()-1));
		for(int i = end.size()-1;i > 0 ;i --){
			System.out.println("第"+(end.size()-i)+"步--------------------");
			end.get(i).getNum().show();
		}
		/*for(int i = 0;i < result.size();i ++){
			System.out.println("第"+(i+1)+"步--------------------");
			result.get(i).getNum().show();
		}*/
		
		System.out.println("第"+(end.size())+"步--------------------");
		for(int i = 0;i < 3;i ++){
			for(int j = 0;j < 3;j ++){
				if(Goal[i][j] == 0){
					System.out.print("  ");
				}
				else
				System.out.print(Goal[i][j]+" ");
			}
			System.out.println("");
		}
		System.out.println("一共查詢了"+result.size()+"個點.");
		System.out.println("一共產生了"+total+"個點.");
	}
	
	private void delEnd(Node node){		//除去多餘的步驟,採用遞迴,從後向前根據父節點找出最短的路徑
		end.add(node);
		if(node.getFather()!=null){
			delEnd(node.getFather());
		}
	}
	
	private void sort() {	//氣泡排序,由大到小
		// TODO Auto-generated method stub
		Node node;
		if(nodeList.size()>1)
			for(int i = 0;i < nodeList.size();i ++){
				for(int j = i+1;j < nodeList.size();j ++){
					if(nodeList.get(i).getValue()<nodeList.get(j).getValue()){
						node = nodeList.get(i);
						nodeList.set(i, nodeList.get(j));
						nodeList.set(j, node);
					}
				}
			}
	}
}

這裡是節點類Node:

/**
 * 八數碼的每個節點,節點內容包括
 * 資料num
 * 深度depth
 * 總權重value
 * 每個位置距離目標位置的和weight
 * 兩個建構函式,引數是輸入的字串,因為只有初始狀態會用到這個建構函式,所以深度會是0
 * 另一個建構函式,Num物件和深度depth
 * 具有的方法有
 * 從輸入的資訊提取資料num的getNum
 * 
 * @author 41571
 *
 */
public class Node {
	private Num num;
	private int depth = 0;
	private int weight = 0;
	private int value = 0;
	private Node father;
	
	public Node(Num num,int depth,Node father){		//中間遍歷需要產生的節點,這裡需要記錄父節點
		this.num = num;
		this.weight = this.num.getWeight();
		this.depth = depth;
		this.value = this.depth + this.weight;
		this.father = father;
	}
	public Node(String numString){		//用來產生最初的根節點,這裡不需要父節點
		this.num = getNum(numString);
		this.weight = this.num.getWeight();
		this.depth = 0;
		this.value = this.depth + this.weight;
	}
	
	public Num getNum(String numString){	//把一行String資料轉化為int型陣列
		int[][] num = new int[3][3];	//二維陣列
		int[] nums = new int[9];	//一維陣列
		char[] numChar;
		numChar = numString.toCharArray();
		int a = 0;
		for(int i = 0;i < numChar.length;i++){
			if(' ' != numChar[i]){
				nums[a++] = numChar[i];
			}
		}
		a = 0;
		for(int j = 0;j < nums.length;j++){
			num[a][j%3] = nums[j] - 48;
			if(j%3 == 2)
				a++;
		}
		return new Num(num);
	}
	public Num getNum(){
		return this.num;
	}
	public int getDepth(){
		return this.depth;
	}
	public int getWeight(){
		return this.weight;
	}
	public int getValue(){
		return this.value;
	}
	public Node getFather(){
		return this.father;
	}
}

為了方便對節點所儲存的狀態的操作,我新增了Num類,用來實現對具體八數碼的數字的移動:

/**
 * 資料類,存放資料
 * 具有的功能,空格的上下左右的移動函式
 * up,down,lift,right
 * 以及移動是否安全,isUp,isDown,isLift,isRight
 * 以及顯示函式show
 * 還有獲得每個位置距離目標位置的和的函式getWeight
 * @author 41571
 *
 */

public class Num {
	private int [][] data;
	private int [] zero = new int[2];	//0點所在的位置
	public Num(int[][] data){
		this.data = new int[3][3];
		this.data = data;
		this.zero = this.getZero();
	}
	public void show(){		//列印
		for(int i = 0;i < this.data.length;i ++){
			for(int j = 0;j < this.data[i].length;j ++){
				if(data[i][j] == 0) System.out.print("  ");
				else System.out.print(data[i][j]+" ");
			}
			System.out.println("");
		}
	}
	public boolean isUp(){		//能上移嗎
		if(zero[0] >= 1) return true;
		return false;
	}
	public Num up(){	//上移
		if(zero[0] >= 1){
			this.data[zero[0]][zero[1]] = this.data[zero[0]-1][zero[1]];
			this.data[zero[0]-1][zero[1]] = 0;
		}
		return this;
	}
	public boolean isDown(){	//能下移嗎
		if(zero[0] <= 1) return true;
		else return false;
	}
	public Num down(){		//下移
		if(zero[0] <= 1){
			this.data[zero[0]][zero[1]] = this.data[zero[0]+1][zero[1]];
			this.data[zero[0]+1][zero[1]] = 0;
		}
		return this;
	}
	public boolean isLift(){		//能左移嗎
		if(zero[1] >= 1) return true;
		else return false;
	}
	public Num lift(){		//左移
		if(zero[1] >= 1){
			this.data[zero[0]][zero[1]] = this.data[zero[0]][zero[1]-1];
			this.data[zero[0]][zero[1]-1] = 0;
		}
		return this;
	}
	public boolean isRight(){		//能右移嗎
		if(zero[1] <= 1) return true;
		else return false;
	}
	public Num right(){		//右移
		if(zero[1] <= 1){
			this.data[zero[0]][zero[1]] = this.data[zero[0]][zero[1]+1];
			this.data[zero[0]][zero[1]+1] = 0;
		}
		return this;
	}
	public int[] getZero(){	//獲得空格即是0所在位置
		int a = 0;
		for(int i = 0;i < this.data.length;i ++){
			for(int j = 0;j < this.data[i].length;j ++){
				if(this.data[i][j] == 0){
					zero[0] = i;
					zero[1] = j;
					a = 1;
					break;
				}
			}
			if(a == 1) break;
		}
		if(a == 0)	System.out.println("這個八數碼中沒有空格");
		return zero;
	}
	public int getWeight(){ 	//獲得每個位置距目標位置的和
		int total = 0;
		for(int i = 0;i < this.data.length;i ++){
			for(int j = 0;j < this.data[i].length;j ++){
				//System.out.print("查詢"+i+","+j+" 資料 "+data[i][j]);
				if(data[i][j]!=0)
					total+=getEveryoneWeight(i,j);
			}
		}
		return total;
	}
	private int getEveryoneWeight(int i, int j) {		//獲得每個格子上的數距原位置的移動步數
		// TODO Auto-generated method stub
		int row = 0,col = 0,a = 0;
		for(int m = 0;m < Project.Goal.length;m ++){
			for(int n = 0;n < Project.Goal[m].length;n ++){
				if(this.data[i][j] == Project.Goal[m][n]){
					//System.out.println("  目標在"+m+","+n+" 資料"+Project.Goal[m][n]);
					row = i - m;
					col = j - n;
					a = 1;
					break;
				}
			}
			if(a == 1) break;
		}
		if(row < 0) row = 0 - row;
		if(col < 0) col = 0 - col;
		return (row + col);
	}
	public int[][] getData(){
		return this.data;
	}
	public boolean isSame(int[][] same){	//判斷是否一樣
		for(int i = 0;i <same.length;i ++){
			for(int j = 0;j < same.length;j ++){
				if(same[i][j]!=this.data[i][j])
					return false;
			}
		}
		return true;
	}
}


相關文章