啓發式搜索求解八數碼問題

問題的定義

又稱九宮問題。在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展開的公式:
X=an(n1)!+an1(n2)!+an1(n3)!+...+a10!X = a_n * (n-1)! + a_{n-1}*(n-2)! + a_{n-1} * (n-3)!+...+a_1*0!
其中,aia_i爲整數,且0<=ai<i,1<=i<=n0 <= a_i < i, 1<=i<=n

aia_i表示原數的第i位在當前未出現的元素中是排在第幾個。

**康拓展開可以求一個排列是所有排列中的第幾大。**相當於把排列和十進制數做了映射,求出了某個排列的id。


例如:1,2,3(1, 2, 3)組成的排列,怎樣知道x=213x = 213是所有排列中的第幾大的數?

  • a1=2a_1 = 2,比2小的只有1,那麼小於2開頭的數有12!1*2!個。第一個數固定是1,第二個數有兩種可能的情況,第三個數只有一種情況。
    在這裏插入圖片描述
  • a2=1a_2 = 1,比1小的數沒有(1~n的排列),那麼小於第二位爲1的數有010*1!
  • 因此小於213的(1,2,3)(1, 2, 3)排列數有12!+01!=21*2! + 0*1! = 2個,可知213是第3大的數,可以認爲數213的id爲2。
  • 12!+01!=21*2! + 0*1! = 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),n=3n = 3id=2id = 2做逆康託展開:

  • 2/2!=12/2! = 1餘0,則a3=1a_3 = 1,可知比第一位小的數有1個,所以首位爲2
  • 0/1!=00/1! = 0餘1,則a2=1a_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

那麼這個序列的逆序值就是0+0+2+0+0+2+1+1=60+0+2+0+0+2+1+1 = 6

結論:
如果兩個狀態的數字序列的逆序值奇偶性一致,則兩狀態互相可達。

證明肯定是不會證明的…

啓發函數

啓發式搜索就是利用知識來引導搜索,儘量避免搜索無效的搜索、減少搜索範圍,降低時間複雜度。啓發信息的強弱對搜索的過程有重大影響。

對於九宮格問題啓發信息一般有兩種,分別是:

  • 取目標狀態與當前狀態相同的節點個數
  • 當前狀態每個結點到目標狀態相應結點所需步數的總和(曼哈頓距離)

感覺兩個都比較合理,但是第二個更加合適。

因爲在第一種算出來相同的情況下,往往需要再看第二種誰的步數總和小說明哪個解更優秀。

open表和close表

  • open表中用來記錄考察過的點
  • close表中用來記錄當前待考察的點

因爲已經實現了每個狀態都用一個十進制數id標識,那麼open表用一個int型的數組就可以了。

而close表需要時刻按照待考察的解的啓發信息高低來排序,比較適合存在一個堆裏。

其他

爲了最後輸出步驟,需要一個int型數組parent來指示這個狀態是怎樣從前一個狀態到達的,也就是走的方向,故需要記錄前驅結點信息(包括id和方向;

搜索過程

參考文檔

Created with Raphaël 2.2.0開始將初始狀態放入close表從close表中取出“最具潛力”的狀態(解)該狀態是目標狀態?輸出搜索步驟結束擴展該狀態,並將擴展的狀態加入close表yesno

結果演示

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

源代碼

Java實現(IDE爲IDEA)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章