『壹』 求8數碼A或A*演算法(用C語言)
//dfs+hash+記憶化 爐灰出品
#include "stdio.h"
#define N 4
#define swap(a,b) a=a+b;b=a-b;a=a-b;
int map[N][N]/*={{0,0,0,0},{0,1,2,3},{0,8,0,4},{0,7,6,5}}*/,hashta[99991]={0},path[2000]={0},min=150,book[N][N]={0},H=0,step=0,choos[5]={0},len=0;
int hash(int x,int y,int temp[N][N])
{ int i=0,j=0,k=1,sum=0;
for(i=1;i<N;i++)
{ for(j=1;j<N;j++)
{ sum=sum*x+temp[i][j]*k-2;
k*=(y+4);
}
}
return sum;
}
void dfs(int x,int y)
{ int i=0,j=0,k=0,p=0,mark=0,temp=0,h=0;
h=hash(x,y,book)%99991;
if(step>min) return;
if(h==H)
{ if( step<min )
{ min=step;
//printf("%d\n",min);
//getch();
}
hashta[h]++;
return ;
}
if( hashta[h]!=0 ) {return ;}
hashta[h]=1;
if(x-1>0)
{ step++;
swap(book[x-1][y],book[x][y])
dfs(x-1,y);
swap(book[x-1][y],book[x][y])
step--;
}
if(x+1<N)
{ step++;
swap(book[x+1][y],book[x][y])
dfs(x+1,y);
swap(book[x+1][y],book[x][y])
step--;
}
if(y-1>0)
{ step++;
swap(book[x][y-1],book[x][y])
dfs(x,y-1);
swap(book[x][y-1],book[x][y])
step--;
}
if(y+1<N)
{ step++;
swap(book[x][y+1],book[x][y])
dfs(x,y+1);
swap(book[x][y+1],book[x][y])
step--;
}
hashta[h]=0;
}
int main(void)
{ int i=0,j=0,mark=0,k=0,p=0,x=0,y=0;
freopen("八數碼.in","r",stdin);
freopen("八數碼.out","w",stdout);
for(i=1;i<N;i++)
{ for(j=1;j<N;j++)
{ scanf("%d",&map[i][j]);
}
}
for(i=1;i<N;i++)
{ for(j=1;j<N;j++)
{ scanf("%d",&book[i][j]);
if(book[i][j]==0) {x=i;y=j;}
}
}
H=hash(2,2,map)%99991;
dfs(x,y);
if(hashta[H]!=0) printf("%d\n",min);
else printf("-1\n");
return 0;
}
因為原程序和此題稍有不同而臨時修改,所以可能還有點問題。 qiji8816你自己再看看吧,反正修改前是可以運行的。^_^
『貳』 求課設代碼
基於A演算法求解八數碼問題是一種規劃問題,即用有限步驟把初始狀態轉換成目標狀態的過程。A演算法是一種帶有啟發式函數的搜索演算法,用於通過估價函數指導搜索,提高搜索效率。
為了實現上述功能,需要定義若干個變數和函數,如下所示:
定義變數:
init_state:初始狀態,即八數碼問題的初始排列;
goal_state:目標狀態,即八數碼問題的最終排列;
f_score:估價函數值,即啟發函數和實際代價之和;
g_score:實際代價,即當前狀態到起始狀態的實際步數;
h_score:啟發函數值,即當前狀態到目標狀態的估計步數。
定義函數:
get_successor_states():用於獲取當前狀態的所有後繼狀態;
get_heuristic_value():用於計算當前狀態到目標狀態的估計步數,即啟發函數值;
get_g_value():用於計算當前狀態到起始狀態的實際步數;
get_f_value():用於計算當前狀態的估價函數值,即啟發函數值與實際代價之和;
compare_f_values():用於比較兩個狀態的估價函數值的大小;
A*_search():用於執行A*搜索演算法,求解八數碼問題。
在定義了上述變數和函數後,我們就可以編寫A*搜索演算法的代碼了。下面是一個可能的實現方式:
Copy code
# 定義估價函數
def get_heuristic_value(state):
h_value = 0
# 計算估價函數值
for i in range(len(state)):
for j in range(len(state[i])):
if state[i][j] != goal_state[i][j]:
h_value += 1
return h_value
# 定義實際代價函數
def get_g_value(state):
g_value = 0
# 計算實際代價值
for i in range(len(state)):
for j in range(len(state[i])):
if state[i][j] != init_state[i][j]:
g_value += 1
return g_value
# 定義估價函數值函數
def get_f_value(state):
# 計算估價函數值
f_value = get_g_value(state) + get_heuristic_value(state)
return f_value
# 定義估價函數值比較函數
def compare_f_values(state1, state2):
# 比較兩個狀態的估價函數值
f1 = get_f_value(state1)
f2 = get_f_value(state2)
if f1 < f2:
return -1
elif f1 > f2:
return 1
else:
return 0
# 定義A*搜索演算法
def A*_search():
# 初始化OPEN表和CLOSED表
open_list = []
closed_list = []
# 將初始狀態加入OPEN表
並更新估價函數值 open_list.append(init_state) f_values[init_state] = get_f_value(init_state)
# 循環執行直到OPEN表為空
while len(open_list) > 0:
# 從OPEN表中選擇估價函數值最小的狀態
current_state = min(open_list, key=get_f_value)
# 如果當前狀態是目標狀態,則演算法執行成功
if current_state == goal_state:
return True
# 將當前狀態從OPEN表中移除,並加入CLOSED表
open_list.remove(current_state)
closed_list.append(current_state)
# 獲取當前狀態的所有後繼狀態
successor_states = get_successor_states(current_state)
# 遍歷所有後繼狀態
for successor_state in successor_states:
# 如果後繼狀態已在CLOSED表中,則跳過
if successor_state in closed_list:
continue
# 如果後繼狀態不在OPEN表中,則加入OPEN表並更新估價函數值
if successor_state not in open_list:
open_list.append(successor_state)
f_values[successor_state] = get_f_value(successor_state)
# 如果新的路徑更優,則更新估價函數值
elif get_f_value(successor_state) < f_values[successor_state]:
f_values[successor_state] = get_f_value(successor_state)
# 如果OPEN表為空,則演算法執行失敗
return False
上面的代碼實現了A*搜索演算法的基本流程,包括初始化、主循環、狀態擴展和結束條件判斷。它使用了估價函數值來比較
不同狀態的優劣,從而決定搜索的方向和順序。
為了實現上述需求,我們還需要對代碼進行一些改進和完善,如下所示:
定義3種不同的啟發式函數:我們可以定義3種不同的啟發式函數,分別用於計算曼哈頓距離、歐幾里得距離和拼圖曼哈頓距離。這些啟發式函數的實現方式略有不同,但都基於當前狀態與目標狀態之間的位置關系進行計算。
提供可視化界面:我們可以創建一個可視化界面,用於展示搜索過程。該界面應能夠顯示搜索樹、估價函數值、OPEN表和CLOSED表的動態變化情況。同時,用戶應能夠選擇預定義的啟發式函數,隨機初始化初始狀態,單步執行或連續執行搜索演算法。
統計擴展節點數和執行時間:為了對採用不同啟發式函數的A*演算法進行性能對比研究,我們需要統計演算法執行過程中的擴展節點數和執行時間。這些信息可以用來評估演算法的效率和優劣。
通過上述
改進和完善,我們就可以實現一個能夠求解八數碼問題的A*演算法,具有較好的可視化展示和性能分析能力。
『叄』 問: 40 人工智慧及其應用期末作業 用A*演算法解決下面的八數碼難題。試定義估價函數,啟發函數,
#pragma warning(disable:4786)
#include <algorithm>
#include <cstdio>
#include <set>
#include <utility>
#include <ctime>
#include <cassert>
#include <cstring>
#include <iostream>
using namespace std;
/*item記錄搜索空間中一個結點
state 記錄用整數形式表示的8數碼格局
blank 記錄當前空格位置,主要用於程序優化,
擴展時可不必在尋找空格位置
g, h 對應g(n), h(n)
pre 記錄當前結點由哪個結點擴展而來 */
struct item
{
int state;
int blank;
int g;
int h;
int pre;
};
const int MAXSTEPS = 100000;
const int MAXCHAR = 100;
char buf[MAXCHAR][MAXCHAR]; //open表
item open[MAXSTEPS];
//vector<item> open;
int steps = 0;
//closed表,已查詢狀態只要知道該狀態以及它由哪個結點擴展而來即可,用於輸出路徑
//每次只需得到對應f值最小的待擴展結點,用堆實現提高效率
pair<int, int> closed[MAXSTEPS];
//讀入,將8數碼矩陣格局轉換為整數表示
bool read(pair<int,int> &state)
{
if (!gets(buf[0]))
return false;
if (!gets(buf[1]))
return false;
if (!gets(buf[2]))
return false;
//cout << strlen(buf[0]) << ' ' << strlen(buf[1]) << ' ' << strlen(buf[2]) << endl;
assert(strlen(buf[0]) == 5 && strlen(buf[1]) == 5 && strlen(buf[2]) == 5);
// astar.in中的每行數據長度必須為5
state.first = 0;
for (int i = 0, p = 1; i < 3; ++i)
{
for (int j = 0; j < 6; j += 2)
{
if (buf[i][j] == '0')
state.second = i * 3 + j / 2; // state.second為0(空格)在節點中的位置
else
state.first += p * (buf[i][j] - '0');
p *= 10;
}
}
/* 若初試節點為:
1 2 3
8 0 4
7 6 5
則state.first為567408321,state.second為4
*/
return true;
}
//計算當前結點距目標的距離
int calculate(int current, int target) // return h=the sum of distances each block have to move to the right position,這里的each block不包括空格
{
int c[9], t[9];
int i, cnt = 0;
for (i = 0; i < 9; ++i)
{
c[current % 10] = t[target % 10] = i;
current /= 10;
target /= 10;
}
for (i = 1; i < 9; ++i)
cnt += abs(c[i] / 3 - t[i] / 3) + abs(c[i] % 3 - t[i] % 3);
return cnt;
}
//open表中結點間選擇時的規則 f(n) = g(n) + h(n)
class cmp
{
public: inline bool operator()(item a, item b)
{
return a.g + a.h > b.g + b.h;
}
};
//將整數形式表示轉換為矩陣表示輸出
void pr(int state)
{
memset(buf, ' ', sizeof(buf));
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 6; j += 2)
{
if (state % 10)
buf[i][j] = state % 10 + '0';
state /= 10;
}
buf[i][5] = '\0';
puts(buf[i]);
}
}
//用於判斷當前空格是否可以向對應方向移動
inline bool suit(int a, int b) //空格移動後的坐標為(a,b)
{
return (a >= 0 && a < 3 && b >= 0 && b < 3);
}
//遞歸輸出搜索路徑
void path(int index)
{
if (index == 0)
{
pr(closed[index].first);
puts("");
return;
}
path(closed[index].second);
pr(closed[index].first); //將整數形式表示轉換為矩陣表示輸出
puts("");
++steps;
}
int getNixuNum( int state ) //求節點的逆序對數
{
int sum = 0;
int result[9];
memset( result, 0, sizeof(result) );
//cout << result[8] << result[7] << endl;
char buf[10];
itoa( state, buf, 10 );
//cout << buf << endl;
int k = 0;
while( buf[k] != '\0' )
{
result[9-k-1] = buf[k] - '0';
k++;
}
for( int i = 0; i < 9; i++ )
{
for( int j = i + 1; j < 9; j++ )
{
if( result[i] && result[j] && result[i] > result[j] )
{
sum++;
}
}
}
return sum; //返回3*3方格數組的逆序對數
}
int main()
{
//cout << getNixuNum(87654321);
//open.resize(MAXSTEPS);
unsigned int t1 = clock();
//cout << open.size() << endl;
if( freopen("astar.in", "r", stdin) == NULL )
{
cout << "file not find\n";
exit(0);
};
freopen("astar2.out", "w", stdout);
set<int>states;
char tmp[100];
int i, x, y, a, b, nx, ny, end, next, index, kase = 0;
pair<int,int> start, target;
item head; //4個方向移動時的偏移量
const int xtran[4] = {-1, 0, 1, 0};
const int ytran[4] = {0, 1, 0, -1};
const int p[] = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000};
while (read(start)) // 讀取初試狀態節點
{
unsigned int t2 = clock();
printf("Case %d:\n\n", ++kase);
gets(tmp);
read(target); // 讀取目標狀態節點
gets(tmp);
int targetNixuNum = getNixuNum(target.first);
//若兩者的逆序對數不是同為奇數或同為偶數,則無解
if( !(getNixuNum(start.first)&1 && targetNixuNum&1 || !(getNixuNum(start.first)&1) && !(targetNixuNum&1)) )
{
cout << "無法從初始節點到終態節點\n";
exit(0);
}
//初始化open表,將初始狀態加入
open[0].state = start.first;
open[0].h = calculate(start.first, target.first); // 計算當前節點到目標節點的估計距離
open[0].blank = start.second;
open[0].pre = -1; // 初始節點無父節點
open[0].g = 0; // 初始節點的g為0
index = 0;
states.insert(start.first); // 擴展過節點保存在states中,即出現過的狀態保存在states中,states為set<int>類型,其中的states中的元素唯一
//提取open表中f值最小元素放入closed表,並對該結點進行擴展
for (end = 1; end > 0; ++index) // end為open表中的元素個數,一直循環到open表為空
{
assert(index < MAXSTEPS);
//臨時存儲
head = open[0]; // 由於使用pop_heap函數和push_heap函數,所以open[0]為g+h最小的元素
//放入closed表記錄當前格局和由哪個結點擴展而來(該結點肯定已在closed表中)
closed[index].first = open[0].state; //放入close表中,表示已經擴展完的節點,下面的for循環會擴展其節點
closed[index].second = open[0].pre; // index表示當前close表中當前擴展節點的下標
//從open表中刪除該結點
pop_heap(open, open + end, cmp());//為algorithm文件中的函數,第一個參數指定開始位置,第二個指定結束,第三個指定比較函數
--end;
//得到結果,遞歸輸出路徑
if (head.state == target.first)
{
path(index);
break;
}
x = head.blank / 3;
y = head.blank % 3; //空格在3*3方格中的x,y坐標
/*
|2 0 3|
A = |1 8 4|
|7 6 5| // 看成3*3的數組
則head.blank=1
x=0,y=1,即空格的在3*3的數組中下標為(0,1)
*/
for (i = 0; i < 4; ++i)
{
nx = x + xtran[i];
ny = y + ytran[i];
/*
i=0時:(nx,ny)為當前空格向上移動一格後的坐標
i=1時:(nx,ny)為當前空格向右移動一格後的坐標
i=2時:(nx,ny)為當前空格向下移動一格後的坐標
i=3時:(nx,ny)為當前空格向左移動一格後的坐標
*/
if (suit(nx, ny)) // 判斷是否能夠移動
{
a = head.blank; // 空格當前位置,以上面矩陣A為例,a=1
b = nx * 3 + ny; // 空格移動後的新位置,開始是能夠向右邊移動,故b=0*3+2=2
//調換十進製表示整數對應兩個數字位
next = head.state + ((head.state % p[a + 1]) / p[a] - (head.state % p[b + 1]) / p[b]) * p[b] + ((head.state % p[b + 1]) / p[b] - (head.state % p[a + 1]) / p[a]) * p[a];
// 如head.state=567481302,空格向右移動一次後,next=567481032,即為移動後的節點
// 判斷能否由當前節點到達目標節點
if( ( getNixuNum(next)&1 && targetNixuNum&1 ) || ( !(getNixuNum(next)&1) && !(targetNixuNum&1) ) )
{
//判斷是否已經擴展過,即已經出現過
if (states.find(next) == states.end()) //沒出現就保存一下,也存入open表
{
states.insert(next);
open[end].pre = index; //擴展後的子節點,其父節點為當前擴展節點
open[end].blank = b;
open[end].state = next;
open[end].h = calculate(next,target.first);
open[end].g = head.g + 1;
++end; //open表中元素加1
push_heap(open, open + end, cmp()); //壓入堆中
}
}
}
}
}
if (end <= 0)
puts("No solution");
else
{
printf("Num of steps: %d\n", steps);
printf("Num of expanded: %d\n", index);
printf("Num of generated: %d\n", index + end);
printf("Time consumed: %d\n\n", clock() - t2);
}
states.clear();
steps = 0;
}
printf("Total time consumed: %d\n", clock() - t1);
return 0;
}
『肆』 為什麼八數碼問題用a*演算法求解合適
其實A*演算法也是一種最好優先的演算法只不過要加上一些約束條件罷了。由於在一些問題求解時,我們希望能夠求解出狀態空間搜索的最短路徑,也就是用最快的方法求解問題,A*就是干這種事情的!我們先下個定義,如果一個估價函數可以找出最短的路徑,我們稱之為可採納性。A*演算法是一個可採納的最好優先演算法。A*演算法的估價函數可表示為:f'(n)=g'(n)+h'(n)這里,f'(n)是估價函數,g'(n)是起點到節點n的最短路徑值,h'(n)是n到目標的最短路經的啟發值。由於這個f'(n)其實是無法預先知道的,所以我們用前面的估價函數f(n)做近似。g(n)代替g'(n),但g(n)>=g'(n)才可(大多數情況下都是滿足的,可以不用考慮),h(n)代替h'(n),但h(n)<=h'(n)才可(這一點特別的重要)。可以證明應用這樣的估價函數是可以找到最短路徑的,也就是可採納的。我們說應用這種估價函數的最好優先演算法就是A*演算法。舉一個例子,其實廣度優先演算法就是A*演算法的特例。其中g(n)是節點所在的層數,h(n)=0,這種h(n)肯定小於h'(n),所以由前述可知廣度優先演算法是一種可採納的。實際也是。當然它是一種最臭的A*演算法。再說一個問題,就是有關h(n)啟發函數的信息性。h(n)的信息性通俗點說其實就是在估計一個節點的值時的約束條件,如果信息越多或約束條件越多則排除的節點就越多,估價函數越好或說這個演算法越好。這就是為什麼廣度優先演算法的那麼臭的原因了,誰叫它的h(n)=0,一點啟發信息都沒有。但在游戲開發中由於實時性的問題,h(n)的信息越多,它的計算量就越大,耗費的時間就越多。就應該適當的減小h(n)的信息,即減小約束條件。但演算法的准確性就差了,這里就有一個平衡的問題。
『伍』 C++ A* 演算法實現中,怎樣便捷地實現「搜索節點在OPEN表和CLOSED表之間 切換」
你是在用A*演算法做八數碼問題么。
比如你要在open表中刪除一個節點,然後放到close表中,你可以反過來做,
先找到這個結點,在close表中插入,然後再在open表中刪除
我把我寫的windows下的代碼貼過來你看看,順便說一句,open表和close表都要經常做刪除節點的操作,所以用vector(線性表)其實性能不好,我用的List。
『陸』 求八數碼問題演算法,並說明下該演算法優缺點,要演算法,不是源代碼(可以沒有)。
八數碼問題
一.八數碼問題
八數碼問題也稱為九宮問題。在3×3的棋盤,擺有八個棋子,每個棋子上標有1至8的某一數字,不同棋子上標的數字不相同。棋盤上還有一個空格,與空格相鄰的棋子可以移到空格中。要求解決的問題是:給出一個初始狀態和一個目標狀態,找出一種從初始轉變成目標狀態的移動棋子步數最少的移動步驟。所謂問題的一個狀態就是棋子在棋盤上的一種擺法。棋子移動後,狀態就會發生改變。解八數碼問題實際上就是找出從初始狀態到達目標狀態所經過的一系列中間過渡狀態。
八數碼問題一般使用搜索法來解。搜索法有廣度優先搜索法、深度優先搜索法、A*演算法等。這里通過用不同方法解八數碼問題來比較一下不同搜索法的效果。
二.搜索演算法基類
1.八數碼問題的狀態表示
八數碼問題的一個狀態就是八個數字在棋盤上的一種放法。每個棋子用它上面所標的數字表示,並用0表示空格,這樣就可以將棋盤上棋子的一個狀態存儲在一個一維數組p[9]中,存儲的順序是從左上角開始,自左至右,從上到下。也可以用一個二維數組來存放。
2.結點
搜索演算法中,問題的狀態用結點描述。結點中除了描述狀態的數組p[9]外,還有一個父結點指針last,它記錄了當前結點的父結點編號,如果一個結點v是從結點u經狀態變化而產生的,則結點u就是結點v的父結點,結點v的last記錄的就是結點u的編號。在到達目標結點後,通過last 可以找出搜索的路徑。
3.類的結構
在C++中用類來表示結點,類將結點有關的數據操作封裝在一起。
不同的搜索演算法具有一定共性,也有各自的個性,因此這里將不同搜索演算法的共有的數據和功能封裝在一個基類中,再通過繼承方式實現不同的搜索演算法。
4.結點擴展規則
搜索就是按照一定規則擴展已知結點,直到找到目標結點或所有結點都不能擴展為止。
八數碼問題的結點擴展應當遵守棋子的移動規則。按照棋子移動的規則,每一次可以將一個與空格相鄰棋子移動到空格中,實際上可以看作是空格作相反移動。空格移動的方向可以是右、下、左、上,當然不能移出邊界。棋子的位置,也就是保存狀態的數組元素的下標。空格移動後,它的位置發生變化,在不移出界時,空格向右、下、左和上移動後,新位置是原位置分別加上1、3、-1、-3,如果將空格向右、下、左和上移動分別用0、1、2、3表示,並將-3、3、-1、1放在靜態數組d[4]中,空格位置用spac表示,那麼空格向方向i移動後,它的位置變為spac+d[i]。空格移動所產生的狀態變化,反映出來則是將數組p[]中,0的新位置處的數與0交換位置。
5.八數碼問題的基類
八數碼問題的基類及其成員函數的實現如下:
#define Num 9
class TEight
{
public:
TEight(){}
TEight(char *fname); //用文件數據構造節點
virtual void Search()=0; //搜索
protected:
int p[Num];
int last,spac;
static int q[Num],d[],total;
void Printf();
bool operator==(const TEight &T);
bool Extend(int i);
};
int TEight::q[Num];//儲存目標節點
int TEight::d[]={1,3,-1,-3};//方向
int TEight::total=0;//步數
TEight::TEight(char *fname)
{
ifstream fin;
fin.open(fname,ios::in);
if(!fin)
{
cout<<"不能打開數據文件!"<<endl;
return;
}
int i;
for(i=0;i<Num;)//得到源節點
fin>>p[i++];
fin>>spac;
for(i=0;i<Num;)//得到目標節點
fin>>q[i++];
fin.close();
last=-1;
total=0;
}
void TEight::Printf()//把路徑列印到結果文件
{
ofstream fout;
fout.open("eight_result.txt",ios::ate|ios::app);
fout<<total++<<"t";
for(int i=0;i<Num;)
fout<<" "<<p[i++];
fout<<endl;
fout.close();
}
bool TEight::operator==(const TEight &T)//判斷兩個狀態是否相同
{
for(int i=0;i<Num;)
if(T.p[i]!=p[i++])
return 0;
return 1;
}
bool TEight::Extend(int i)//擴展
{
if(i==0 && spac%3==2 || i==1 && spac>5
|| i==2 && spac%3==0 || i==3 && spac<3)
return 0;
int temp=spac;
spac+=d[i];
p[temp]=p[spac];
p[spac]=0;
return 1;
}
數據文件的結構:
一共三行,第一行是用空格隔開的九個數字0~8,這是初始狀態。第二行是一個數字,空格(數字0)的位置,第三行也是用空格隔開的九個數字0~8,這是目標狀態。
三.線性表
搜索法在搜索過程中,需要使用一個隊列存儲搜索的中間結點,為了在找到目標結點後,能夠找到從初始結點到目標結點的路徑,需要保留所有搜索過的結點。另一方面,不同問題甚至同一問題的不同搜索方法中,需要存儲的結點數量相差很大,所以這里採用鏈式線性表作為存儲結構,同時,為適應不同問題,線性表設計成類模板形式。
template<class Type> class TList; //線性表前視定義
template<class Type> class TNode //線性表結點類模板
{
friend class TList<Type>;
public:
TNode(){}
TNode(const Type& dat);
private:
TNode<Type>* Next;
Type Data;
};
template<class Type> class TList
{
public:
TList(){Last=First=0;Length=0;} //構造函數
int Getlen()const{return Length;} //成員函數,返回線性表長度
int Append(const Type& T); //成員函數,從表尾加入結點
int Insert(const Type& T,int k); //成員函數,插入結點
Type GetData(int i); //成員函數,返回結點數據成員
void SetData(const Type& T,int k); //成員函數,設置結點數據成員
private:
TNode<Type> *First,*Last; //數據成員,線性表首、尾指針
int Length; //數據成員,線性表長度
};
template<class Type> int TList<Type>::Append(const Type& T)
{
Insert(T,Length);
return 1;
}
template<class Type> int TList<Type>::Insert(const Type& T,int k)
{
TNode<Type> *p=new TNode<Type>;
p->Data=T;
if(First)
{
if(k<=0)
{
p->Next=First;
First=p;
}
if(k>Length-1)
{
Last->Next=p;
Last=Last->Next;
Last->Next=0;
}
if(k>0 && k<Length)
{
k--;
TNode<Type> *q=First;
while(k-->0)
q=q->Next;
p->Next=q->Next;
q->Next=p;
}
}
else
{
First=Last=p;
First->Next=Last->Next=0;
}
Length++;
return 1;
}
template<class Type> Type TList<Type>::GetData(int k)
{
TNode<Type> *p=First;
while(k-->0)
p=p->Next;
return p->Data;
}
template<class Type> void TList<Type>::SetData(const Type& T,int k)
{
TNode<Type> *p=First;
while(k-->0)
p=p->Next;
p->Data=T;
}
線性表單獨以頭文件形式存放。
四.廣度優先搜索法
在搜索法中,廣度優先搜索法是尋找最短路經的首選。
1.廣度優先搜索演算法的基本步驟
1)建立一個隊列,將初始結點入隊,並設置隊列頭和尾指針
2)取出隊列頭(頭指針所指)的結點進行擴展,從它擴展出子結點,並將這些結點按擴展的順序加入隊列。
3)如果擴展出的新結點與隊列中的結點重復,則拋棄新結點,跳至第六步。
4)如果擴展出的新結點與隊列中的結點不重復,則記錄其父結點,並將它加入隊列,更新隊列尾指針。
5)如果擴展出的結點是目標結點,則輸出路徑,程序結束。否則繼續下一步。
6)如果隊列頭的結點還可以擴展,直接返回第二步。否則將隊列頭指針指向下一結點,再返回第二步。
2.搜索路徑的輸出
搜索到目標結點後,需要輸出搜索的路徑。每個結點有一個數據域last,它記錄了結點的父結點,因此輸出搜索路徑時,就是從目標結點Q出發,根據last找到它的父結點,再根據這個結點的last找到它的父結點,....,最後找到初始結點。搜索的路徑就是從初始結點循相反方向到達目標結點的路徑。
3.廣度優先搜索法TBFS類的結構
廣度優先搜索法TBFS類是作為TEight類的一個子類。其類的結構和成員函數的實現如下:
class TBFS:public TEight
{
public:
TBFS(){}
TBFS(char *fname):TEight(fname){}
virtual void Search();
private:
void Printl(TList<TBFS> &L);
int Repeat(TList<TBFS> &L);
int Find();
};
void TBFS::Printl(TList<TBFS> &L)
{
TBFS T=*this;
if(T.last==-1)
return;
else
{
T=L.GetData(T.last);
T.Printl(L);
T.Printf();
}
}
int TBFS::Repeat(TList<TBFS> &L)
{
int n=L.Getlen();
int i;
for(i=0;i<n;i++)
if(L.GetData(i)==*this)
break;
return i;
}
int TBFS::Find()
{
for(int i=0;i<Num;)
if(p[i]!=q[i++])
return 0;
return 1;
}
void TBFS::Search()
{
TBFS T=*this;
TList<TBFS> L;
L.Append(T);
int head=0,tail=0;
while(head<=tail)
{
for(int i=0;i<4;i++)
{
T=L.GetData(head);
if(T.Extend(i) && T.Repeat(L)>tail)
{
T.last=head;
L.Append(T);
tail++;
}
if(T.Find())
{
T.Printl(L);
T.Printf();
return;
}
}
head++;
}
}
4.廣度優先搜索法的缺點
廣度優先搜索法在有解的情形總能保證搜索到最短路經,也就是移動最少步數的路徑。但廣度優先搜索法的最大問題在於搜索的結點數量太多,因為在廣度優先搜索法中,每一個可能擴展出的結點都是搜索的對象。隨著結點在搜索樹上的深度增大,搜索的結點數會很快增長,並以指數形式擴張,從而所需的存儲空間和搜索花費的時間也會成倍增長。
五、A*演算法
1.啟發式搜索
廣度優先搜索和雙向廣度優先搜索都屬於盲目搜索,這在狀態空間不大的情況下是很合適的演算法,可是當狀態空間十分龐大時,它們的效率實在太低,往往都是在搜索了大量無關的狀態結點後才碰到解答,甚至更本不能碰到解答。
搜索是一種試探性的查尋過程,為了減少搜索的盲目性引,增加試探的准確性,就要採用啟發式搜索了。所謂啟發式搜索就是在搜索中要對每一個搜索的位置進行評估,從中選擇最好、可能容易到達目標的位置,再從這個位置向前進行搜索,這樣就可以在搜索中省略大量無關的結點,提高了效率。
2.A*演算法
A*演算法是一種常用的啟發式搜索演算法。
在A*演算法中,一個結點位置的好壞用估價函數來對它進行評估。A*演算法的估價函數可表示為:
f'(n) = g'(n) + h'(n)
這里,f'(n)是估價函數,g'(n)是起點到終點的最短路徑值(也稱為最小耗費或最小代價),h'(n)是n到目標的最短路經的啟發值。由於這個f'(n)其實是無法預先知道的,所以實際上使用的是下面的估價函數:
f(n) = g(n) + h(n)
其中g(n)是從初始結點到節點n的實際代價,h(n)是從結點n到目標結點的最佳路徑的估計代價。在這里主要是h(n)體現了搜索的啟發信息,因為g(n)是已知的。用f(n)作為f'(n)的近似,也就是用g(n)代替g'(n),h(n)代替h'(n)。這樣必須滿足兩個條件:(1)g(n)>=g'(n)(大多數情況下都是滿足的,可以不用考慮),且f必須保持單調遞增。(2)h必須小於等於實際的從當前節點到達目標節點的最小耗費h(n)<=h'(n)。第二點特別的重要。可以證明應用這樣的估價函數是可以找到最短路徑的。
3.A*演算法的步驟
A*演算法基本上與廣度優先演算法相同,但是在擴展出一個結點後,要計算它的估價函數,並根據估價函數對待擴展的結點排序,從而保證每次擴展的結點都是估價函數最小的結點。
A*演算法的步驟如下:
1)建立一個隊列,計算初始結點的估價函數f,並將初始結點入隊,設置隊列頭和尾指針。
2)取出隊列頭(隊列頭指針所指)的結點,如果該結點是目標結點,則輸出路徑,程序結束。否則對結點進行擴展。
3)檢查擴展出的新結點是否與隊列中的結點重復,若與不能再擴展的結點重復(位於隊列頭指針之前),則將它拋棄;若新結點與待擴展的結點重復(位於隊列頭指針之後),則比較兩個結點的估價函數中g的大小,保留較小g值的結點。跳至第五步。
4)如果擴展出的新結點與隊列中的結點不重復,則按照它的估價函數f大小將它插入隊列中的頭結點後待擴展結點的適當位置,使它們按從小到大的順序排列,最後更新隊列尾指針。
5)如果隊列頭的結點還可以擴展,直接返回第二步。否則將隊列頭指針指向下一結點,再返回第二步。
4.八數碼問題的A*演算法的估價函數
估價函數中,主要是計算h,對於不同的問題,h有不同的含義。那麼在八數碼問題中,h的含意是各什麼?八數碼問題的一個狀態實際上是數字0~8的一個排列,用一個數組p[9]來存儲它,數組中每個元素的下標,就是該數在排列中的位置。例如,在一個狀態中,p[3]=7,則數字7的位置是3。如果目標狀態數字3的位置是8,那麼數字7對目標狀態的偏移距離就是3,因為它要移動3步才可以回到目標狀態的位置。
八數碼問題中,每個數字可以有9個不同的位置,因此,在任意狀態中的每個數字和目標狀態中同一數字的相對距離就有9*9種,可以先將這些相對距離算出來,用一個矩陣存儲,這樣只要知道兩個狀態中同一個數字的位置,就可查出它們的相對距離,也就是該數字的偏移距離:
0 1 2 3 4 5 6 7 8
0 0 1 2 1 2 3 2 3 4
1 1 0 1 2 1 2 3 2 3
2 2 1 0 3 2 1 4 3 2
3 1 2 3 0 1 2 1 2 3
4 2 1 2 1 0 1 2 1 2
5 3 2 1 2 1 0 3 2 1
6 2 3 4 1 2 3 0 1 2
7 3 2 3 2 1 2 1 0 1
8 4 3 2 3 2 1 2 1 0
例如在一個狀態中,數字8的位置是3,在另一狀態中位置是7,那麼從矩陣的3行7列可找到2,它就是8在兩個狀態中的偏移距離。
估價函數中的h就是全體數字偏移距離之和。顯然,要計算兩個不同狀態中同一數字的偏移距離,需要知道該數字在每個狀態中的位置,這就要對數組p[9]進行掃描。由於狀態發生變化,個數字的位置也要變化,所以每次計算h都沿線掃描數組,以確定每個數字在數組中的位置。為了簡化計算,這里用一個數組存儲狀態中各個數字的位置,並讓它在狀態改變時隨著變化,這樣就不必在每次計算h時,再去掃描狀態數組。
例如,某個狀態中,數字5的位置是8,如果用數組r[9]存儲位置,那麼就有r[5]=8。
現在用數組r[9]存儲當前狀態的數字位置,而用s[9]存儲目標狀態的數字位置,那麼當前狀態數字i對目標狀態的偏移距離就是矩陣中r[i]行s[i]列對應的值。
5.A*演算法的類結構
A*演算法的類聲明如下:
class TAstar:public TEight
{
public:
TAstar(){} //構造函數
TAstar(char *fname); //帶參數構造函數
virtual void Search(); //A*搜索法
private:
int f,g,h; //估價函數
int r[Num]; //存儲狀態中各個數字位置的輔助數組
static int s[Num]; //存儲目標狀態中各個數字位置的輔助數組
static int e[]; //存儲各個數字相對距離的輔助數組
void Printl(TList<TAstar> L); //成員函數,輸出搜索路徑
int Expend(int i); //成員函數,A*演算法的狀態擴展函數
int Calcuf(); //成員函數,計算估價函數
void Sort(TList<TAstar>& L,int k); //成員函數,將新擴展結點按f從小到大順序插入待擴展結點隊列
int Repeat(TList<TAstar> &L); //成員函數,檢查結點是否重復
};
int TAstar::s[Num],TAstar::e[Num*Num];
TAstar::TAstar(char *fname):TEight(fname)
{
for(int i=0;i<Num;)
{
r[p[i]]=i; //存儲初始狀態個個數字的位置
s[q[i]]=i++; //存儲目標狀態個個數字的位置
}
ifstream fin;
fin.open("eight_dis.txt",ios::in); //打開數據文件
if(!fin)
{
cout<<"不能打開數據文件!"<<endl;
return;
}
for(int i=0;i<Num*Num;i++) //讀入各個數字相對距離值
fin>>e[i];
fin.close();
f=g=h=0; //估價函數初始值
}
void TAstar::Printl(TList<TAstar> L)
{
TAstar T=*this;
if(T.last==-1) return;
else
{
T=L.GetData(T.last);
T.Printl(L);
T.Printf();
}
}
int TAstar::Expend(int i)
{
if(Extend(i)) //結點可擴展
{
int temp=r[p[r[0]]]; //改變狀態後數字位置變化,存儲改變後的位置
r[p[r[0]]]=r[0];
r[0]=temp;
return 1;
}
return 0;
}
int TAstar::Calcuf()
{
h=0;
for(int i=0;i<Num;i++) //計算估價函數的 h
h+=e[Num*r[i]+s[i]];
return ++g+h;
}
void TAstar::Sort(TList<TAstar>& L,int k)
{
int n=L.Getlen();
int i;
for(i=k+1;i<n;i++)
{
TAstar T=L.GetData(i);
if(this->f<=T.f)
break;
}
L.Insert(*this,i);
}
int TAstar::Repeat(TList<TAstar> &L)
{
int n=L.Getlen();
int i;
for(i=0;i<n;i++)
if(L.GetData(i)==*this)
break;
return i;
}
void TAstar::Search()
{
TAstar T=*this; //初始結點
T.f=T.Calcuf(); //初始結點的估價函數
TList<TAstar> L; //建立隊列
L.Append(T); //初始結點入隊
int head=0,tail=0; //隊列頭和尾指針
while(head<=tail) //隊列不空則循環
{
for(int i=0;i<4;i++) //空格可能移動方向
{
T=L.GetData(head); //去隊列頭結點
if(T.h==0) //是目標結點
{
T.Printl(L);//輸出搜索路徑
T.Printf(); //輸出目標狀態
return; //結束
}
if(T.Expend(i)) //若結點可擴展
{
int k=T.Repeat(L); //返回與已擴展結點重復的序號
if(k<head) //如果是不能擴展的結點
continue; //丟棄
T.last=head; //不是不能擴展的結點,記錄父結點
T.f=T.Calcuf(); //計算f
if(k<=tail) //新結點與可擴展結點重復
{
TAstar Temp=L.GetData(k);
if(Temp.g>T.g) //比較兩結點g值
L.SetData(T,k); //保留g值小的
continue;
}
T.Sort(L,head) ; //新結點插入可擴展結點隊列
tail++; //隊列尾指針後移
}
}
head++; //一個結點不能再擴展,隊列頭指針指向下一結點
}
}
六、測試程序
A*演算法的測試:
int main()
{
TAstar aStar("eight.txt");
aStar.Search();
system("pauze");
return 0;
}
eight.txt文件中的數據(初始態和目標態):
一共三行,第一行是用空格隔開的九個數字0~8,這是初始狀態。第二行是一個數字,空格(數字0)的位置,第三行也是用空格隔開的九個數字0~8,這是目標狀態。
8 3 5 1 2 7 4 6 0
8
1 2 3 4 5 6 7 8 0
eight_dis.txt中的數據(估計函數使用)
0 1 2 1 2 3 2 3 4
1 0 1 2 1 2 3 2 3
2 1 0 3 2 1 4 3 2
1 2 3 0 1 2 1 2 3
2 1 2 1 0 1 2 1 2
3 2 1 2 1 0 3 2 1
2 3 4 1 2 3 0 1 2
3 2 3 2 1 2 1 0 1
4 3 2 3 2 1 2 1 0
eight_Result.txt中的結果(運行後得到的結果)
七、演算法運行結果
1.BFS演算法只能適用於到達目標結點步數較少的情況,如果步數超過15步,運行時間太長,實際上不再起作用。
2.對於隨機生成的同一個可解狀態,BFS演算法最慢,DBFS演算法較慢,A*演算法較快。但在15步以內,DBFS演算法與A*演算法相差時間不大,超過15步後,隨步數增加,A*演算法的優勢就逐漸明顯,A*演算法要比DBFS演算法快5倍以上,並隨步數增大而增大。到25步以上,DBFS同樣因運行時間過長而失去價值。
3.一般來說,解答的移動步數每增加1,程序運行時間就要增加5倍以上。由於八數碼問題本身的特點,需要檢查的節點隨步數增大呈指數形式增加,即使用A*演算法,也難解決移動步數更多的問題。
八、問題可解性
八數碼問題的一個狀態實際上是0~9的一個排列,對於任意給定的初始狀態和目標,不一定有解,也就是說從初始狀態不一定能到達目標狀態。因為排列有奇排列和偶排列兩類,從奇排列不能轉化成偶排列或相反。
如果一個數字0~8的隨機排列871526340,用F(X)表示數字X前面比它小的數的個數,全部數字的F(X)之和為Y=∑(F(X)),如果Y為奇數則稱原數字的排列是奇排列,如果Y為偶數則稱原數字的排列是偶排列。
例如871526340這個排列的
Y=0+0+0+1+1+3+2+3+0=10
10是偶數,所以他偶排列。871625340
Y=0+0+0+1+1+2+2+3+0=9
9是奇數,所以他奇排列。
因此,可以在運行程序前檢查初始狀態和目標狀態的窘是否相同,相同則問題可解,應當能搜索到路徑。否則無解。
PS:整理自網路
『柒』 什麼是啟發式搜索並以八數碼難題為例,說明其原理
啟發式搜索就是在狀態空間中的搜索對每一個搜索的位置進行評估,得到最好的位置,再從這個位置進行搜索直到目標。這樣可以省略大量無謂的搜索路徑,提高了效率。在啟發式搜索中,對位置的估價是十分重要的。採用了不同的估價可以有不同的效果。我們先看看估價是如何表示的。
啟發中的估價是用估價函數表示的,如:
最佳優先搜索的最廣為人知的形式稱為A*搜索(發音為「A星搜索」).它把到達節點的耗散g(n)
和從該節點到目標節點的消耗h(n)結合起來對節點進行評價:f(n)=g(n)+h(n)
因為以g(n)給出了從起始節點到節點n的路徑耗散,而h(n)是從節點n到目標節點的最低耗散路徑的估計耗散值,因此f(n)=經過節點n的最低耗散解的估計耗散.這樣,如果我們想要找到最低耗散解,首先嘗試找到g(n)+h(n)值最小的節點是合理的。可以發現這個策略不只是合理的:倘若啟發函數h(n)滿足一定的條件,A*搜索既是完備的也是最優的。
如果把A*搜索用於Tree-Search,它的最優性是能夠直接分折的。在這種情況下,如果h(n)是一個可採納啟發式--也就是說,倘若h(n)從不會過高估計到達目標的耗散--A*演算法是最優的。可採納啟發式天生是最優的,因為他們認為求解問題的耗散是低於實際耗散的。因為g(n)是到達節點n的確切耗散,我們得到一個直接的結論:f(n)永遠不會高估經過節點n的解的實際耗散.
啟發演算法有:
蟻群演算法,遺傳演算法、模擬退火演算法等
蟻群演算法是一種來自大自然的隨機搜索尋優方法,是生物界的群體啟發式行為,現己陸續應用到組合優化、人工智慧、通訊等多個領域。蟻群演算法的正反饋性和協同性使其可用於分布式系統,隱含的並行性更使之具有極強的發展潛力。從數值模擬結果來看,它比目前風行一時的遺傳演算法、模擬退火演算法等有更好的適應性。
『捌』 人工智慧技術A*演算法解決八數碼問題的實驗
八數碼 估價函數可以選h(s)=ΣΣ[|i-⌊s[i,j]-1)/3⌋| + |j-(s[i,j]-1)mod3|]
『玖』 深度優先演算法解決八數碼問題
首先我們來想像一隻老鼠,在一座不見天日的迷宮內,老鼠在入口處進去,要從出 口出來。那老鼠會怎麼走?當然是這樣的:老鼠如果遇到直路,就一直往前走,如果遇到分叉路口,就任意選 擇其中的一個繼續往下走,如果遇到死胡同,就退回到最近的一個分叉路口,選擇另一條道路再走下去,如果 遇到了出口,老鼠的旅途就算結束了。深度優先搜索法的基本原則就是這樣:按照某種條件往前試探搜索,如 果前進中遭到失敗(正如老鼠遇到死胡同)則退回頭另選通路繼續搜索,直到找到條件的目標為止。
實現這一演算法,我們要用到編程的另一大利器--遞歸。遞歸是一個很抽象的概念, 但是在日常生活中,我們還是能夠看到的。拿兩面鏡子來,把他們面對著面,你會看到什麼?你會看到鏡子中 有無數個鏡子?怎麼回事?A鏡子中有B鏡子的象,B鏡子中有A鏡子的象,A鏡子的象就是A鏡子本身的真實寫 照,也就是說A鏡子的象包括了A鏡子,還有B鏡子在A鏡子中的象………………好累啊,又煩又繞口,還不好理 解。換成計算機語言就是A調用B,而B又調用A,這樣間接的,A就調用了A本身,這實現了一個重復的功能。再 舉一個例子;從前有座山,山裡有座廟,廟里有個老和尚,老和尚在講故事,講什麼呢?講:從前有座山,山 里有座廟,廟里有個老和尚,老和尚在講故事,講什麼呢?講:從前有座山,山裡有座廟,廟里有個老和尚, 老和尚在講故事,講什麼呢?講:…………。好傢伙,這樣講到世界末日還講不玩,老和尚講的故事實際上就 是前面的故事情節,這樣不斷地調用程序本身,就形成了遞歸。 萬一這個故事中的某一個老和尚看這個故事不 順眼,就把他要講的故事換成:「你有完沒完啊!」,這樣,整個故事也就嘎然而止了。我們編程就要注意這 一點,在適當的時候,就必須要有一個這樣的和尚挺身而出,把整個故事給停下來,或者使他不再往深一層次 搜索,要不,我們的遞歸就會因計算機存儲容量的限制而被迫溢出,切記,切記。
我們把遞歸思想運用到上面的迷宮中,記老鼠現在所在的位置是(x,y),那它現在有 前後左右4個方向可以走,分別是(x+1,y),(x-1,y),(x,y+1),(x,y-1),其中一個方向是它來時的路,我們先不 考慮,我們就分別嘗試其他三個方向,如果某個方向是路而不是牆的話,老鼠就向那個方向邁出一步。在新的 位置上,我們又可以重復前面的步驟。老鼠走到了死胡同又是怎麼回事?就是除了來時的路,其他3個方向都是 牆,這時這條路就走到了盡頭,無法再向深一層發展,我們就應該沿來時的路回去,嘗試另外的方向。
例:八皇後問題:在標准國際象棋的棋盤上(8*8格)准備放置8隻皇後,我們知 道,國際象棋中皇後的威力是最大的,她既可以橫走豎走,還可以斜著走,遇到擋在她前進路線上的敵人,她 就可以吃掉對手。要求在棋盤上安放8隻皇後,使她們彼此互相都不能吃到對方,求皇後的放法。
這是一道很經典的題目了,我們先要明確一下思路,如何運用深度優先搜索法,完 成這道題目。我們先建立一個8*8格的棋盤,在棋盤的第一行的任意位置安放一隻皇後。緊接著,我們就來放 第二行,第二行的安放就要受一些限制了,因為與第一行的皇後在同一豎行或同一對角線的位置上是不能安放 皇後的,接下來是第三行,……,或許我們會遇到這種情況,在擺到某一行的時候,無論皇後擺放在什麼位 置,她都會被其他行的皇後吃掉,這說明什麼呢?這說明,我們前面的擺放是失敗的,也就是說,按照前面 的皇後的擺放方法,我們不可能得到正確的解。那這時怎麼辦?改啊,我們回到上一行,把原先我們擺好的 皇後換另外一個位置,接著再回過頭擺這一行,如果這樣還不行或者上一行的皇後只有一個位置可放,那怎 么辦?我們回到上一行的上一行,這和老鼠碰了壁就回頭是一個意思。就這樣的不斷的嘗試