問題的定義
又稱九宮問題。在3×3的棋盤上,擺有八個棋子,每個棋子上標有1至8的某一數字,不同棋子上標的數字不相同。棋盤上還有一個空格,空格可以不超過邊界地上下左右移動。
要求解決的問題是:以啓發式搜索方法求解給定初始狀態和目標狀態的最優搜索路徑。
例如,那麼空格應該向上走一步:
問題的解決
解的表示
將九宮格中數字從上到下、從左到右順序排列後形成一個9個數字的序列(空格用0表示)來標識九宮格的一種狀態。那麼初始狀態爲:
1 | 2 | 3 | 4 | 0 | 5 | 6 | 7 | 8 |
---|
目標狀態爲:
1 | 0 | 3 | 4 | 2 | 5 | 6 | 7 | 8 |
---|
那麼如何唯一的確定一個狀態?這裏用到了康託展開。
康託展開
cantor展開的公式:
其中,爲整數,且。
表示原數的第i位在當前未出現的元素中是排在第幾個。
**康拓展開可以求一個排列是所有排列中的第幾大。**相當於把排列和十進制數做了映射,求出了某個排列的id。
例如:組成的排列,怎樣知道是所有排列中的第幾大的數?
- ,比2小的只有1,那麼小於2開頭的數有個。第一個數固定是1,第二個數有兩種可能的情況,第三個數只有一種情況。
- ,比1小的數沒有(1~n的排列),那麼小於第二位爲1的數有
- 因此小於213的排列數有個,可知213是第3大的數,可以認爲數213的id爲2。
- 就是一個康託展開式,實現了排列到十進制的映射。
const int factorial[]={1,1,2,6,24,120,720,5040,40320,362880,3628800};//階乘0-10
//cantor展開,n表示是n位的全排列,num[]表示全排列的數(用數組表示)
int cantor(int num[],int n){
int ans=0,sum=0; //sum中存放a[i]的值
for(int i=1;i<n;i++){
for(int j=i+1;j<=n;j++)
if(num[j]<num[i])
sum++;
ans+=sum*factorial[n-i];//累積
sum=0;//計數器歸零
}
return ans+1;
}
逆康託展開
知道一個排列是第幾大的數,同樣可以反過來求出這個排列是什麼。
但是注意反向求時用的是id,而不是第幾大。比如剛剛的213是第3大的數,id是2。
那麼對排列(1, 2, 3), 和做逆康託展開:
- 餘0,則,可知比第一位小的數有1個,所以首位爲2
- 餘1,則,比第二位小的數有0個,所以第二位爲1
- 自然最後一個數就是3了
- 得到了213,實現了十進制到排列的映射
//康託展開逆運算
void decantor(int x, int n)
{
vector<int> v; // 存放當前可選數
vector<int> a; // 所求排列組合
for(int i=1;i<=n;i++)
v.push_back(i);
for(int i=n;i>=1;i--)
{
int r = x % factorial[i-1];
int t = x / factorial[i-1];
x = r;
sort(v.begin(),v.end());// 從小到大排序
a.push_back(v[t]); // 剩餘數裏第t+1個數爲當前位
v.erase(v.begin()+t); // 移除選做當前位的數
}
for(int i=0; i<a.size(); i++){
cout<<a[i]<<" ";
}
}
那麼就可以把每個狀態用一個十進制數id來表示了,並能夠方便的判斷兩個狀態是否相同,搜索過程中經常要判斷是否搜索到了目標狀態。
需要注意的是,因爲空格我是用0表示,所以康託展開和逆康託展開是對於0到n-1的排列做,和1到n的計算過程有一點點區別,當然也可以直接用9表示空格。
不可達狀態的識別
如果用戶輸入的初始狀態和目標狀態本身不可達,而我們又能提前識別出這種不可達情況,就可以避免很多無謂的嘗試和計算。
可以用兩個狀態的序列逆序值的奇偶性來判斷是否可達。注意判斷逆序性時不考慮0。
- 數的逆序值:位於這個數前面的比這個數大的數的個數。
- 序列的逆序值:數列中每個數的逆序值之和
比如:
序列值 | 2 | 3 | 1 | 5 | 8 | 4 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
逆序值 | 0 | 0 | 2 | 0 | 0 | 2 | 1 | 1 |
那麼這個序列的逆序值就是
結論:
如果兩個狀態的數字序列的逆序值奇偶性一致,則兩狀態互相可達。
證明肯定是不會證明的…
啓發函數
啓發式搜索就是利用知識來引導搜索,儘量避免搜索無效的搜索、減少搜索範圍,降低時間複雜度。啓發信息的強弱對搜索的過程有重大影響。
對於九宮格問題啓發信息一般有兩種,分別是:
- 取目標狀態與當前狀態相同的節點個數
- 當前狀態每個結點到目標狀態相應結點所需步數的總和(曼哈頓距離)
感覺兩個都比較合理,但是第二個更加合適。
因爲在第一種算出來相同的情況下,往往需要再看第二種誰的步數總和小說明哪個解更優秀。
open表和close表
- open表中用來記錄考察過的點
- close表中用來記錄當前待考察的點
因爲已經實現了每個狀態都用一個十進制數id標識,那麼open表用一個int型的數組就可以了。
而close表需要時刻按照待考察的解的啓發信息高低來排序,比較適合存在一個堆裏。
其他
爲了最後輸出步驟,需要一個int型數組parent來指示這個狀態是怎樣從前一個狀態到達的,也就是走的方向,故需要記錄前驅結點信息(包括id和方向;
搜索過程
結果演示