八數碼問題解析


八數碼的問題描述為:
在3×3的棋盤上,擺有八個棋子,每個棋子上標有1至8的某一數字。棋盤中留有一個空格,空格用-1來表示。空格周圍的棋子可以移到空格中。要求解的問題是:給出一種初始布局(初始狀態)和目標布局,找到一種最少步驟的移動方法,實現從初始布局到目標布局的轉變。

解決八數碼的方法很多,本文采用1.廣度優先搜索的策略,和A星算法兩種比較常用的算法思想解決此問題
廣度優先搜索的策略一般可以描述為以下過程:

狀態空間的一般搜索過程
OPEN表:用於存放剛生成的節點
CLOSE表:用於存放將要擴展或已擴展的節點

  1. 把初始節點S0放入OPEN表,並建立只含S0的圖,記為G
    OPEN:=S0,G:=G0(G0=S0)
  2. 檢查OPEN表是否為空,若為空則問題無解,退出
    LOOP:IF(OPEN)=() THEN EXIT(FAIL)
  3. 把OPEN表的第一個節點取出放入CLOSE表,記該節點為節點n
    N:=FIRST(OPEN),REMOVE(n,OPEN),ADD(n,CLOSE)
  4. 觀察節點n是否為目標節點,若是,則求得問題的解,退出
    IF GOAL(n) THEN EXIT(SUCCESS)
  5. 擴展節點n,生成一組子節點.把其中不是節點n先輩的那些子節點記作集合M,並把這些節點作為節點n的子節點加入G中.
    EXPAND(n)-->M(mi),G:=ADD(mi,G)
  6. 轉第2步

下面貼出代碼:

import time
import copy

class list():
    def __init__(self,info):
        self.info = info
        self.front = None
            
class Solution():
    def __init__(self):
        self.open = []
        self.closed = []
        self.co = 0

    def msearch(self,S0, Sg):
        head = list(S0)
        self.open.append(head)
        while self.open:
            n = self.open.pop(0)
            self.co += 1
            print('取得節點n:',n.info) 
            if n.info == Sg:
                print('得到問題的解!')
                print('一共進行了',self.co,'次查找')
                print('該問題的解為:') #對n進行
                while n:
                    print(n.info)
                    n = n.front
                return
            if n in self.closed:
                #節點判定是否為擴展問題
                print('該結點不可擴展') 
            else:
                print('該節點可擴展')
                #擴展節點n,
                #將其子節點放入open的尾部,
                #為每一個子節點設置指向父節點的指針
                nkongdi, nkongdj = 0, 0
                for i in range(3):
                    for j in range(3):
                        if n.info[i][j] == -1:
                            nkongdi = i
                            nkongdj = j
                ln,un,rn,dn =copy.deepcopy(n.info),copy.deepcopy(n.info),copy.deepcopy(n.info),copy.deepcopy(n.info)
                if nkongdj != 0: #right
                    rn[nkongdi][nkongdj],rn[nkongdi][nkongdj-1] = rn[nkongdi][nkongdj-1],rn[nkongdi][nkongdj]
                    rn = self.link(n,rn)
                    if rn not in self.closed:
                        self.open.append(rn)
                if nkongdi != 0: #down
                    dn[nkongdi][nkongdj],dn[nkongdi-1][nkongdj] = dn[nkongdi-1][nkongdj],dn[nkongdi][nkongdj]
                    dn = self.link(n,dn)
                    if dn not in self.closed:
                        self.open.append(dn)
                if nkongdj != 2: #left
                    ln[nkongdi][nkongdj],ln[nkongdi][nkongdj+1] = ln[nkongdi][nkongdj+1],ln[nkongdi][nkongdj]
                    ln = self.link(n,ln)
                    if ln not in self.closed:
                        self.open.append(ln)
                if nkongdi != 2: #up
                    un[nkongdi][nkongdj],un[nkongdi+1][nkongdj] =  un[nkongdi+1][nkongdj],un[nkongdi][nkongdj]
                    un = self.link(n,un)
                    if un not in self.closed:
                        self.open.append(un)  
            self.closed.append(n)
    
    def link(self, n ,willn):
        willnn = list(willn)
        willnn.front = n
        return willnn

if __name__ == '__main__':
    S0 = [[2,8,3],
         [1,-1,4],
         [7,6,5]]
    S1 = [[1,2,3],
          [8,-1,4],
          [7,6,5]]
    Solution().msearch(S0,S1)
  

代碼的一些問題:
1.對於八數碼這種狀態,可以采用一些常用的壓縮策略來減少對內存空間的使用。本文為簡單其間,並為采用壓縮策略,直接以數組表示。
2.對於查找n的后繼節點,應該是可以采用一個循環來減少代碼冗余的。
此方法優點,缺點:
1.完備的策略->必定會找到一個解
2.找到的解必定是路徑最短的解
3.盲目性大,搜索效率低

為了解決以上,盲目性大,搜索效率低的問題:我們引出A算法。 A算法的原理如下:

A* [1] (A-Star)算法是一種靜態路網中求解最短路徑最有效的直接搜索方法,也是許多其他問題的常用啟發式算法。注意——是最有效的直接搜索算法,之后涌現了很多預處理算法(如ALT,CH,HL等等),在線查詢效率是A*算法的數千甚至上萬倍。
公式表示為: f(n)=g(n)+h(n),
其中, f(n) 是從初始狀態經由狀態n到目標狀態的代價估計,
g(n) 是在狀態空間中從初始狀態到狀態n的實際代價,
h(n) 是從狀態n到目標狀態的最佳路徑的估計代價。

**可以看出,A星算法比上述算法的最大的區別,就是多了這個 估價函數f(n) = 實際代價g(n ) + 估計代價h(n) **

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)的信息,即減小約束條件。但算法的准確性就差了,這里就有一個平衡的問題。

總結下來,其實就是A算法的重點為:在眾多的估計代價函數中的最優的即為A(需證明是最優的)

有了以上的一些結論,可以迅速的改改上述代碼,得到A*算法:

import time
import copy

class list():
    def __init__(self,info):
        self.info = info
        self.fn = 0   #估價:越小越好
        self.front = None

class Solution():
    def __init__(self):
        self.open = []
        self.closed = []
        self.co = 0
    #A算法的重點:估價函數f(n) = 實際代價g(n ) + 估計代價h(n) 
    #A*算法的重點為:在眾多的估計代價函數中的最優的即為A*(需證明是最優的)

    def Asearch(self,S0, Sg):
        head = list(S0)
        head.fn = self.getfn(head,Sg)
        self.open.append(head)
        while self.open:
            #對open表的全部節點按照fn從小到大排序~~~~
            self.open.sort(key=lambda ele:ele.fn)  #默認從小到大
            n = self.open.pop(0)
            self.co += 1
            print('取得節點n:',n.info) 
            if n.info == Sg:
                print('得到問題的解!')
                print('一共進行了',self.co,'次的查找')
                print('該問題的解為:') #對n進行
                while n:
                    print(n.info)
                    n = n.front
                return
            if n in self.closed:
                #節點判定是否為擴展問題
                print('該結點不可擴展') 
            else:
                print('該節點可擴展')
                #擴展節點n,
                #將其子節點放入open的尾部,
                #為每一個子節點設置指向父節點的指針
                nkongdi, nkongdj = 0, 0
                for i in range(3):
                    for j in range(3):
                        if n.info[i][j] == -1:
                            nkongdi = i
                            nkongdj = j
                ln,un,rn,dn =copy.deepcopy(n.info),copy.deepcopy(n.info),copy.deepcopy(n.info),copy.deepcopy(n.info)
                if nkongdj != 0: #right
                    rn[nkongdi][nkongdj],rn[nkongdi][nkongdj-1] = rn[nkongdi][nkongdj-1],rn[nkongdi][nkongdj]
                    rn = self.link(n,rn)
                    if rn not in self.closed:
                        #計算子節點估值~~~
                        rn.fn = self.getfn(rn,Sg)
                        self.open.append(rn)
                if nkongdi != 0: #down
                    dn[nkongdi][nkongdj],dn[nkongdi-1][nkongdj] = dn[nkongdi-1][nkongdj],dn[nkongdi][nkongdj]
                    dn = self.link(n,dn)
                    if dn not in self.closed:
                        dn.fn = self.getfn(dn,Sg)
                        self.open.append(dn)
                if nkongdj != 2: #left
                    ln[nkongdi][nkongdj],ln[nkongdi][nkongdj+1] = ln[nkongdi][nkongdj+1],ln[nkongdi][nkongdj]
                    ln = self.link(n,ln)
                    if ln not in self.closed:
                        ln.fn = self.getfn(ln,Sg)
                        self.open.append(ln)
                if nkongdi != 2: #up
                    un[nkongdi][nkongdj],un[nkongdi+1][nkongdj] =  un[nkongdi+1][nkongdj],un[nkongdi][nkongdj]
                    un = self.link(n,un)
                    if un not in self.closed:
                        un.fn = self.getfn(un,Sg)
                        self.open.append(un)  
            self.closed.append(n)
    
    def link(self, n ,willn):
        willnn = list(willn)
        willnn.front = n
        return willnn

    def h(self, ninfo,Sg):
        #將不再位的A算法和A*算法個數,作為啟發信息
        enlight = 0
        for i in range(3):
            for j in range(3):
                if ninfo[i][j] != Sg[i][j]:
                    enlight += 1
        return enlight

    def g(self,n):
        #g(n) = d(n)
        depth = 0
        while n:
            #print(n.info)
            n = n.front
            depth += 1
        return depth
   
    def getfn(self, n ,Sg):
        #傳入進的是一個listn
        return self.g(n) + self.h(n.info,Sg)

if __name__ == '__main__':
    S0 = [[2,8,3],
         [1,-1,4],
         [7,6,5]]
    S1 = [[1,2,3],
          [8,-1,4],
          [7,6,5]]
    Solution().Asearch(S0,S1)

可以看出A星算法最為重要的就是啟發函數h(n)的選取,h(n)選取的好壞直接關系到了A星算法的好壞。

ps:
A*算法也有一些優化,有興趣的同學也可以看一下- - ~


免責聲明!

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



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