基於搜索策略的八數碼問題求解(報告+源碼)


寫在開頭
這是"人工智能導論"課程的結課作業,里面包括了寬度優先搜索策略全局擇優搜索策略的算法描述與實現,並對於啟發式函數進行了多次對比實驗,主要介紹了6種可行的啟發式函數,希望能給大家帶來一些幫助.

項目源碼見我的GitHub:https://github.com/Jupiter2Pluto/Eight-digit-problem
原文見:https://blog.csdn.net/qq_43393963/article/details/105848728

一、 問題描述

1.1 問題引入

對任意的八數碼問題,給出求解結果。例如對於如下具體八數碼問題:
1	3	2	⇨	1	2	34	 	5		8	 	46	7	8		7	6	5

通過設計啟發函數,編程實現求解過程,如果問題有解,給出數碼移動過程,否則,報告問題無解。

1.2 求解步驟

步驟一 設計八數碼格局的隱式存儲的節點結構

將表示棋局的狀態用如下向量表示:

  • A=(X0,X1 ,X2 ,X3 ,X4 ,X5 , X6 , X7 ,X8)

約束條件:

  • Xi∈{0,1 ,2,3,4,5,6,7,8}(Xi≠Xj,當i≠j時)

初始狀態:

  • S0 =(1,3,2,4,0,5,6,7,8)

目標狀態:

  • Sg =(1,2,3,8,0,4,7,6,5)
步驟二 計算兩個節點之間的可達性

(1)可以通過限定時間閾值或步驟閾值,判定兩個節點之間的可達性。

(2)通過計算八數碼節點的逆序數判斷。如果一對數的前后位置與大小順序相反,即前面的數大於后面的數,那么它們就稱為一個逆序。一個排列中逆序的總數就稱為這個排列的逆序數。逆序數為偶數的排列稱為偶排列;逆序數為奇數的排列稱為奇排列。如2431中,21,43,41,31是逆序,逆序數是4,為偶排列。
計算八數碼節點的逆序數時將代表空格的0去除,例如

初始狀態排列為:(1,3,2,4,5,6,7,8)
逆序數為:0+1+0+0+0+0+0+0=1即為奇排列

目標狀態排列為:(1,2,3,8,4,7,6,5)
逆序數為:0+0+0+4+0+2+1+0=7即為奇排列

具有同奇或同偶排列的八數碼才能移動可達,否則不可達。

步驟三 設計估計函數與啟發函數

估計函數f(n)定義為:f(n)=d(n)+h(n) 其中,d(n)表示節點深度。

啟發函數h(n)可參考如下定義方法:
(1)啟發函數h(n)定義為當前節點與目標節點差異的度量:即當前節點與目標節點格局相比,位置不符的數字個數。
(2)啟發函數h(n)定義為當前節點與目標節點距離的度量:當前節點與目標節點格局相比,位置不符的數字移動到目標節點中對應位置的最短距離之和。
(3)啟發函數h(n)定義為每一對逆序數字乘以一個倍數。
(4)為克服了僅計算數字逆序數字數目策略的局限,啟發函數h(n)定義為位置不符數字個數的總和與3倍數字逆序數目相加。

步驟四 選擇並設計搜索算法(至少選擇1個)

(1)使用盲目搜索中的寬度優先搜索算法。
(2)使用啟發式搜索中的全局擇優搜索算法。
(3)使用A*算法。

步驟五 設計輸入輸出
輸入:
初始節點
目標節點
格式:
1 3 2 4 0 5 6 7 8
1 2 3 8 0 4 7 6 5

可采取以下三種方式輸入:命令行、文件start.txt、GUI輸入。

輸出:如果無解在屏幕輸出"目標狀態不可達";如果有解請在屏幕輸出"最少移動n步到達目標狀態",n為最少移動的步驟數,並記錄從初始狀態到目標狀態的每一步中間狀態,並將這些狀態保存至result.txt文件中。

以上過程可采取以下三種方式輸出:命令行、文件result.txt、GUI輸出。

步驟六 編寫代碼,調試程序

至少給出5組初始節點和目標節點對,並記錄程序運算結果。

節點對要包括以下三種情況:不可達;最少移動步驟數≥10;最少移動步驟數≤6。

1.3 拓展實驗(可任選1個完成)

  1. 記錄步驟四中3個搜索算法的執行時間,並比較3者的效率。

  2. 在啟發式搜索中,分別采用步驟三中啟發式函數(1)(2)(3)(4),並比較四者的效率,思考如何進一步改進啟發式函數。

  3. 試分析在所有可能的初始狀態和目標狀態之間最長的最小移動步驟數是多少。

二、 求解算法設計

2.1 全局擇優搜索算法

(1) 把初始節點S0放入OPEN表,f(S0);
(2) 如果OPEN表為空,則問題無解,退出;
(3) 把OPEN表的第一個節點(記為節點n)取出放入CLOSED表;
(4) 考察節點n是否為目標節點:若是,則求得問題的解,退出;
(5) 若節點n不可擴展,則轉第(2)步;
(6) 擴展節點n,用估計函數f(x)計算每個子節點的估價值,並為每個子節點配置指向父節點的指針;把這些子節點都送入OPEN表中,然后對OPEN表中的全部節點按估價值從小到大的順序進行排序,然后轉第(2)步。

2.2 節點擴展算法

(1) 取該節點八數碼為0的數組下標;
(2) 如果0可以向左移動且父節點不是通過0右移得到的,就左移0生成新的子節點(以防止生成重復節點,下同);
(3) 如果0可以向上移動且父節點不是通過0下移得到的,就上移0生成新的子節點;
(4) 如果0可以向右移動且父節點不是通過0左移得到的,就右移0生成新的子節點;
(5) 如果0可以向下移動且父節點不是通過0上移得到的,就下移0生成新的子節點;

2.3 曼哈頓距離求解算法

在八數碼問題中,某個節點的八數碼與目標節點位置不符的某一個數字移動到目標節點中對應位置的最短距離,即為曼哈頓距離,其求解算法如下:

(1) 取該節點八數碼與目標節點位置不符的某一個數字的數組下標indexA,取該數字在目標節點的數組下標indexB,最終移動步數result=0;
(2) 如果indexA > indexB,則交換兩者的值;
(3) 計算兩者的差differenceValue。
		如果differenceValue≥3,result=1+(indexA+3與indexB的曼哈頓距離);
		如果differenceValue=2,result=2;
		如果differenceValue=0,result=0;
		如果以上均不符合,那么計算(indexA+1)模3的值,如果為0,result取3,否則取1;
(4) 返回result的值。

三、 求解算法實現

3.1 數據結構設計

1.節點結構
struct State{
	int index; // 狀態號
	int zeroIndex;//0的下標
	vector<int>vec; // 8數碼向量
	double func;//估計函數
	int depth;//深度
	int parentIndex;//父節點下標
	int direction;//由父節點如何移動得到:1(左)、2(上)、3(右)、4(下)
};

在八數碼問題中,八數碼的狀態空間表示在程序中定義為一個結構體,記錄該狀態的各種信息:狀態號、空格下標、八數碼排列、估計函數值、深度、父節點下標(狀態號)和父節點移動方向。

2.其它數據結構
list<State>OPEN;//open表
list<State>CLOSED;//closed表
vector<State>allState; // 全局所有的狀態
vector<State>pathOfSolution; // 解路徑

OPEN表和CLOSED表采用鏈表結構,OPEN表用於待考察的節點,CLOSED表用於存放已考察的節點;allState用於存放所有擴展的歷史狀態節點;pathOfSolution用於存放解路徑。

3.2 核心算法實現

3.2.1 全局擇優搜索算法實現

全局擇優搜索算法是啟發式搜索的一種,其主要思想是根據已知狀態得到啟發信息,根據啟發信息輔助進行下一步搜索。其效率的高低主要與啟發式函數的選擇有關,代碼實現過程將啟發式函數封裝,在該搜索算法中直接調用。實現代碼如下:

void globalOptimal()
{
	funcValue(s0);//計算s0的啟發式函數值
	OPEN.push_back(s0); 
	while (!OPEN.empty()) {
		State maxFunc = OPEN.front();//OPEN表第一個節點就是希望最大的(估計函數值最小)
		OPEN.pop_front();
		CLOSED.push_back(maxFunc); 
		if (isTarget(maxFunc.vec)) { //判斷是否是目標節點
			int pathIndex = maxFunc.index;
			do {
		pathOfSolution.insert(pathOfSolution.begin(), allState[pathIndex]);
		pathIndex = allState[pathIndex].parentIndex;
			} while (pathIndex != 0);
			pathOfSolution.insert(pathOfSolution.begin(), allState[0]);
			return;
		}
		int nowStateNum = allState.size();
		extendState(maxFunc);//擴展節點
		// 如果擴展后allState數量不變,則無法擴展
		if (nowStateNum == allState.size()) {continue; }
		for (int i = nowStateNum; i < allState.size(); i++) {
			OPEN.push_back(allState[i]);
		}
		OPEN.sort(riseCmp);	//對OPEN表排序		
	}
	return;
}

3.2.2 節點擴展算法實現

節點的擴展在該問題中極為重要,任何一種利用狀態空間的搜索算法都需要該操作算子,其目的就是將空格(也就是八數碼數組中的0元素)進行移動得到新的節點,該節點就是原來節點的子節點。另外該節點的direction表示該節點的父節點的0通過向哪個方向移動得到:1(左)、2(上)、3(右)、4(下)。
該算法的實現代碼如下:

void extendState(State& s)
{
	int zeroIndex = s.zeroIndex;//取該節點的0的下標	
	if (zeroIndex - 1 >= 0&&(zeroIndex%3!=0)) {//0向左移動
		//如果父節點不是右移得到的,則子節點才能向左移,並生成新節點,下同
		if(s.direction!=3) moveZero(s, 1);}	
	if (zeroIndex - 3 >= 0) {//向上
		if (s.direction != 4) moveZero(s, 2);}	
	if (zeroIndex + 1 < s.vec.size() && ((zeroIndex+1) % 3 != 0)) {//向右
		if (s.direction != 1) moveZero(s, 3);}	
	if (zeroIndex + 3 < s.vec.size()) {//向下
		if (s.direction != 2) moveZero(s, 4);}
}

3.2.3 曼哈頓距離求解算法實現

在八數碼問題中,某個節點的八數碼與目標節點位置不符的某一個數字移動到目標節點中對應位置的最短距離,即為曼哈頓距離,實現代碼如下:

int minMove(int indexA, int indexB)
{
	if (indexA > indexB) {
		int temp = indexA;
		indexA = indexB;
		indexB = temp;	}
	int result = 0;//最終移動步數
	int differenceValue = indexB - indexA; // 下標的差值
	if (differenceValue >= 3) {//如果大於3,即下移(因為一定不在一行)
		result = 1 + minMove(indexA + 3, indexB);//①
	}
	else if (differenceValue == 2) 
		result = 2;//差值等於2,一定只需要移動2步
	else if (differenceValue == 0) 
		result = 0;//差值等於0,不需要移動
	else 
		result = (indexA + 1) % 3 == 0 ? 3 : 1;//② 
	return result;
}

在代碼中有兩處標號,在此說明:
①用遞歸的思想求解,因為最壞的情況是某個數字的下標需要從0移動到8;

②執行該語句時differenceValue必為1,為方便描述,數組下標在八數碼棋盤上的分布如圖3.2.1所示。有2種情況:(i)在同一行,如從1移動到2、從3移動到4等,只需要移動一步,這時indexA(相對於目標位置較小的下標如上面例子的1、3)加1后模3不為0;(ii)不在同一行,如從2移動到3、從5移動到6,需要移動3步,這時indexA(相對於目標位置較小的下標如上面例子的1、3)加1后模3為0。

在這里插入圖片描述

四、 拓展實驗完成情況

本項目完成了第二個拓展實驗,主要完成了如下工作:

  1. 實現了4個不同的啟發式函數進行全局擇優搜索並對啟發式函數進行改進。(詳見4.1節)
  2. 重新定義了2種新的啟發式函數並實現。(詳見4.2節)
  3. 在第二個拓展實驗外,完成了盲目搜索中的寬度優先搜索的實現。(詳見4.3節)

4.1 四種啟發式函數的實現與改進

4.1.1啟發函數h(n)定義為當前節點與目標節點差異的度量

該啟發式函數定義為當前節點與目標節點格局相比,位置不符的數字個數。實現代碼如下:

double heuristicFunc1(State& s){
	int funcValue = 0;
	for (int i = 0; i < target.size(); i++) {
		if (s.vec[i] != target[i]) {(target[i] == 0) ? funcValue : ++funcValue;	}
	}
	return funcValue;
}

4.1.2 啟發函數h(n)定義為當前節點與目標節點距離的度量

該啟發式函數定義為當前節點與目標節點格局相比,位置不符的數字移動到目標節點中對應位置的最短距離之和。實現代碼如下:

double heuristicFunc2(State& s)
{
	int distance = 0;//最終距離
	for (int i = 0; i < s.vec.size(); i++) {
		if ((s.vec[i] != target[i]) && (s.vec[i] != 0)) {
			distance += minMove(i, findVecIndex(target, s.vec[i]));	}
	}
	return distance;
}

4.1.3 啟發函數h(n)定義為每一對逆序數字乘以一個倍數

h(n)定義為每一對逆序數字乘以一個倍數,然后計算該狀態與目標狀態的差值,這個倍數相當於一個權重,一般在2到10之間取值,h(n)=|該狀態逆序數個數-目標狀態逆序數個數|*權重。實現代碼如下:

double heuristicFunc3(State& s,int weight)
{
	return weight*abs(getInverseNum(s.vec) - getInverseNum(target));
}

改進方法:對權重重新定義,定義為逆序數之差。如:3 1,其權重即為3-1=2。即h(n)=|該狀態逆序數權重之和-目標狀態逆序數權重之和|,兩者差值越小,證明該狀態與目標狀態越接近。實現代碼如下:

double heuristicFunc3(State& s)
{
	int inverseNum = 0;
	int targetInverseNum = 0;
	for (int i = 0; i < s.vec.size() - 1; i++) {
		for (int j = i + 1; j < s.vec.size(); j++)
		{
	if (s.vec[i] > s.vec[j] && s.vec[i] && s.vec[j]) 
		inverseNum=inverseNum+ s.vec[i] - s.vec[j];
	if (target[i] > target[j] && target[i] && target[j]) 
		targetInverseNum = targetInverseNum + target[i] - target[j];
		}
	}
	return abs(inverseNum-targetInverseNum);
}

4.1.4啟發函數h(n)定義為位置不符數字個數的總和與3倍數字逆序數目相加

為克服了僅計算數字逆序數字數目策略的局限,啟發函數h(n)定義為位置不符數字個數的總和與3倍數字逆序數目相加,即h(n)=錯位數+3*|該狀態數字逆序數目-目標狀態數字逆序數目|。實現代碼如下:

double heuristicFunc4(State& s)
{
	double differentNum = heuristicFunc1(s);
	double result = 3 * abs(getInverseNum(s.vec) - getInverseNum(target)) + differentNum; 
	return result;
}

4.2 兩種新的啟發式函數並實現

4.2.1 啟發函數h(n)定義為錯位數與相同逆序數對數之差

受到第4個啟發式函數的啟發,可以同時考察錯位數和逆序數,對於逆序數,求取該狀態與目標狀態的相同的逆序數的數量之差,差值越小,說明該狀態與目標狀態越接近。實現代碼如下:

double heuristicFunc5(State& s)
{
	int count = 0;
	for (int i = 0; i < s.vec.size() - 1; i++) {
		for (int j = i + 1; j < s.vec.size(); j++)
		{
			if (s.vec[i] > s.vec[j] && s.vec[i] && s.vec[j]) {
				if (findVecIndex(target, s.vec[i]) < findVecIndex(target, s.vec[j]))
					count++;}	}
	}	
	return heuristicFunc1(s)- count ;
}

4.2.2 啟發函數h(n)定義為錯位的數字移動到目標位的直線距離之和

只考查錯位數字的下標與目標位置的下標之差,作為錯位的數字移動到目標位的直線距離,求出所有距離的和值,結果說明該狀態與目標狀態越接近。雖然該方法忽略了八數碼棋盤的結構,但要優於只考慮錯位數的第一種啟發式函數,因此可以作為一個參考。實現代碼如下:

double heuristicFunc6(State& s)
{
	int funcValue = 0;
	for (int i = 0; i < target.size(); i++) {
		if (s.vec[i] != target[i] && s.vec[i]&& target[i]) {
			funcValue += abs(i - findVecIndex(target, s.vec[i]));}
	}
	return funcValue;
}

4.3 寬度優先搜索

寬度優先搜索算法是一種盲目搜索,從初始節點開始,不斷擴展並將子節點加入OPEN表,直到OPEN表的第一個為目標節點。但效率較低,改進方法是:考察OPEN表的節點,如果(可擴展情況下)擴展該節點,其子節點中含有目標節點則成功返回。其算法描述如下:

(1) 把初始節點S0放入OPEN表;
(2) 初始節點是目標節點?若是,退出;若不是,下一步;
(3) OPEN表為空?若空,失敗退出;
(4) 把OPEN表的第一個節點(記為節點n)取出放入CLOSED表;
(5) 擴展節點n,為每個子節點配置指向父節點的指針,把這些子節點都送入OPEN表中;
(6) 有子節點是目標節點?若有,成功退出;若沒有,轉(3)。

根據上述算法,實現代碼如下:

void widthFirst()
{	
	OPEN.push_back(s0); //初始狀態入OPEN表
	if (isTarget(s0.vec)) { //判斷是否是目標節點
		int pathIndex = s0.index;
		do {//根據生成的目標節點回溯得到解路徑
			pathOfSolution.insert(pathOfSolution.begin(), allState[pathIndex]);
			pathIndex = allState[pathIndex].parentIndex;
		} while (pathIndex != 0);
		pathOfSolution.insert(pathOfSolution.begin(), allState[0]);
		return;
	}
	else {
		do {
			if (OPEN.empty()) { 
				cout << "With First Search Error!" << endl;
				return;
			}
			else {//依次考察OPEN表的第一個節點
				State maxFunc = OPEN.front();
				OPEN.pop_front();
				CLOSED.push_back(maxFunc); 
				int nowStateNum = allState.size();
				extendState(maxFunc);//擴展子節點
				// 如果擴展后allState數量不變,則無法擴展
				if (nowStateNum == allState.size()) {continue;} 
				for (int i = nowStateNum; i < allState.size(); i++) {
					if (isTarget(allState[i].vec)) { //判斷是否是目標節點
						int pathIndex = allState[i].index;
						do {
pathOfSolution.insert(pathOfSolution.begin(), allState[pathIndex]);
pathIndex = allState[pathIndex].parentIndex;
						} while (pathIndex != 0);
						pathOfSolution.insert(pathOfSolution.begin(), allState[0]);
						return;
					}
					OPEN.push_back(allState[i]);
				}
			}
		} while (!OPEN.empty()); 		
	}
} 

五、 實驗對比結果分析

5.1 測試用例1下的結果對比

測試用例1如圖5.1.1所示:
在這里插入圖片描述
該測試用例移動步數≤4,用寬度優先搜索以及全局擇優搜索得到的結果如下表5.1.1所示:
在這里插入圖片描述

首先對於兩種搜索策略,由於移動步數較少,時間上相差較小,但從擴展狀態中可以看出,寬度優先搜索明顯要擴展更多的狀態才能找到目標,而全局擇優在估計函數的輔助下能夠向接近目標地方向上擴展狀態,從而更快地找到目標。

5.2 測試用例2、3下的結果對比

測試用例2(該測試用例移動步數≥10)如圖5.2.1所示:
在這里插入圖片描述

測試用例3(該測試用例移動步數≥20)如圖5.2.2所示:
在這里插入圖片描述

5.2.1 寬度優先與全局擇優的前兩種啟發式函數比較

在測試用例2和3下,將寬度優先與全局擇優的前兩種啟發式函數進行比較,得到的結果如下表5.2.1和表5.2.2所示:

在這里插入圖片描述
從實驗結果可以看出:①當移動步數增加時,全局擇優搜索算法的優勢就能夠體現出來了,無論在擴展狀態上(空間)還是計算時間上,都較寬度優先有明顯優勢。②只考慮錯位數的heuristicFunc1的實現雖然較為簡單,但效果不如heuristicFunc2(曼哈頓距離)好。

5.2.2 第3種啟發式函數(heuristicFunc3)的對比實驗

該啟發式函數h(n)定義為每一對逆序數字乘以一個倍數(權值),實驗將權值從1到10進行結果對比,並將改進的差值權重(逆序數的差值作為該對逆序數的權值)進行對比,在測試用例2和3下結果如下表5.2.3和5.2.4所示:
在這里插入圖片描述
在這里插入圖片描述
由實驗結果可見:①測試用例2下,將權重設為3時效果最好,而改進后的差值權重相對於權重為3的結果又有提升,但解路徑並不是最優,只保證了擴展狀態較少和用時較短。③測試用例3下,將權重設為4時效果最好,而改進的差值權重效果卻下降,猜想原因可能是隨着深度增加,差值權重的影響力相對於深度的影響力小,從而導致擴展了更多的狀態。

為了驗證③中的猜想,又單獨對差值權重進行了幾組對比實驗如表5.2.5,結果如下表5.2.6所示:
在這里插入圖片描述
在這里插入圖片描述

實驗結果看出,③中的猜想正確,但如何控制深度和差值權重的比重還有待進一步研究。

5.2.2 第4、5、6種啟發式函數的對比實驗

這三種啟發式函數下,有可以控制不同的系數來觀察搜索效果,如表5.2.7所示,結果如表5.2.8所示:
在這里插入圖片描述
在這里插入圖片描述
從實驗結果可以看出:①錯位數與逆序數的組合可以較好地尋找目標狀態,而其系數會影響擴展狀態的個數;②不考慮八數碼棋盤的格局情況下,只考慮數組下標位置作為啟發信息依舊能尋找目標解,且在移動步數較多時效果優於只考慮錯位數的啟發式函數。

六、 實驗結論

從以上實驗可以得出結論:

  1. 盲目優先搜索中的寬度優先搜索在移動步數較多情況下其缺點會更加明顯,即不能“智能”地尋找目標解;
  2. 全局擇優搜索算法中,第二種啟發式函數(曼哈頓距離)在移動步數或多或少的情況下都能夠有較好表現;
  3. 對於估價函數,其深度d(n)與啟發式函數h(n)的比重也會影響搜索效率,如果能找到一種比重自適應的方法將會比目前的搜索效率有較大提升;
  4. 本實驗在選取啟發式函數的時候,只考慮了一個節點到目標節點的某種距離或差異的度量,而沒有考慮一個節點在最佳路徑上的概率,因此有部分啟發式函數並不能很好地找到最優解。

七、 個人體會與總結

略略略~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM