使用python實現(xiàn)深度優(yōu)先遍歷搜索(DFS)的示例代碼
1.1 算法介紹
深度優(yōu)先搜索算法(Depth-First-Search,DFS)是一種用于遍歷或搜索樹或圖的算法。沿著樹的深度遍歷樹的節(jié)點,盡可能深的搜索樹的分支。當(dāng)節(jié)點 v 的所在邊都己被探尋過,搜索將回溯到發(fā)現(xiàn)節(jié)點 v 的那條邊的起始節(jié)點。這一過程一直進(jìn)行到已發(fā)現(xiàn)從源節(jié)點可達(dá)的所有節(jié)點為止。如果還存在未被發(fā)現(xiàn)的節(jié)點,則選擇其中一個作為源節(jié)點并重復(fù)以上過程,整個進(jìn)程反復(fù)進(jìn)行直到所有節(jié)點都被訪問為止。屬于盲目搜索。
以上圖為例,簡述 DFS 的過程。首先從根節(jié)點“1”出發(fā),按一定的順序遍歷其子節(jié)點,這里我們假設(shè)優(yōu)先遍歷左邊的。所以,在遍歷“1”之后,我們到了節(jié)點“2”,此時“2”仍有子節(jié)點,所以應(yīng)繼續(xù)向下遍歷,下一個節(jié)點是“3”,然后是“4”。到了“4”之后,沒有子節(jié)點了,說明我們已經(jīng)將這一條路遍歷完了,接著我們應(yīng)該回溯,應(yīng)該回到“4”的父節(jié)點,也就是“3”。因為“3”還有一個子節(jié)點“5”沒有遍歷,所以下一個我們應(yīng)該遍歷的是“5”。遍歷完“5”之后又發(fā)現(xiàn)一條路到頭了,再次回溯依然回溯到其父節(jié)點“3”,此時“3”的所有子節(jié)點都已經(jīng)遍歷完了,因該接著回溯到“3”的父節(jié)點“2”,然后檢查“2”是否有沒有遍歷完的子節(jié)點。按照這樣的規(guī)則,完成所有節(jié)點的遍歷。最終得到的遍歷順序是“1-2-3-4-5-6-7-8-9-10-11-12”
在介紹了 DFS 在遍歷樹的應(yīng)用后,我們將其應(yīng)用于八數(shù)碼問題的解決。八數(shù)碼問題也稱為九宮問題。在 3×3 的棋盤,擺有八個棋子,每個棋子上標(biāo)有 1 至 8 的某一數(shù)字,不同棋子上標(biāo)的數(shù)字不相同。棋盤上還有一個空格,與空格相鄰的棋子可以移到空格中。要求解決的問題是:給出一個初始狀態(tài)和一個目標(biāo)狀態(tài),找出一種從初始轉(zhuǎn)變成目標(biāo)狀態(tài)的移動棋子步數(shù)最少的移動步驟。所謂問題的一個狀態(tài)就是棋子在棋盤上的一種擺法。棋子移動后,狀態(tài)就會發(fā)生改變。解八數(shù)碼問題實際上就是找出從初始狀態(tài)到達(dá)目標(biāo)狀態(tài)所經(jīng)過的一系列中間過渡狀態(tài)。
上面說的 DFS 遍歷的樹是已經(jīng)存在的,我們只需要按照規(guī)定的遍歷方法就能完成遍歷,而對于八數(shù)碼問題,沒有已經(jīng)存在的路徑供我們遍歷,需要我們從初始狀態(tài)向下延伸(也就是上下左右移動)才能構(gòu)造出類似的樹。
以上圖為例。在使用 DFS 進(jìn)行搜索時,每個狀態(tài)都會按照一定的順序進(jìn)行上下左右移動(在上圖中是下、左、右、上的順序),一次移動后會產(chǎn)生一個新的狀態(tài),然后以新狀態(tài)為起點繼續(xù)按約定的順序(例如先向下)移動。終止的條件是找到解或者達(dá)到深度界限。那么如果按照圖中下、左、右、上的順序搜索后的結(jié)果將會是最左邊的一條路一直是優(yōu)先向下移動,如果不能向下則依次會是左、右、上的一種。
1.2 實驗代碼
import copy # 棋盤的類,實現(xiàn)移動和擴(kuò)展?fàn)顟B(tài) class grid: def __init__(self, stat): self.pre = None self.target = [[1, 2, 3], [8, 0, 4], [7, 6, 5]] self.stat = stat self.find0() self.update() #更新深度和距離和 def update(self): self.fH() self.fG() # G是深度,也就是走的步數(shù) def fG(self): if (self.pre != None): self.G = self.pre.G + 1 else: self.G = 0 # H是和目標(biāo)狀態(tài)距離之和,可以用來判斷是否找到解 def fH(self): self.H = 0 for i in range(3): for j in range(3): targetX = self.target[i][j] nowP = self.findx(targetX) self.H += abs(nowP[0] - i) + abs(nowP[1] - j) #以三行三列的形式輸出當(dāng)前狀態(tài) def see(self): print("depth:", self.G) for i in range(3): print(self.stat[i]) print("-" * 10) # 查看找到的解是如何從頭移動的 def seeAns(self): ans = [] ans.append(self) p = self.pre while (p): ans.append(p) p = p.pre ans.reverse() for i in ans: i.see() #找到數(shù)字x的位置 def findx(self, x): for i in range(3): if (x in self.stat[i]): j = self.stat[i].index(x) return [i, j] #找到0的位置,也就是空白格的位置 def find0(self): self.zero = self.findx(0) #對當(dāng)前狀態(tài)進(jìn)行擴(kuò)展,也就是上下左右移動,返回的列表中是狀態(tài)的二維列表,不是對象 def expand(self): i = self.zero[0] j = self.zero[1] gridList = [] if (j == 2 or j == 1): gridList.append(self.left()) if (i == 2 or i == 1): gridList.append(self.up()) if (i == 0 or i == 1): gridList.append(self.down()) if (j == 0 or j == 1): gridList.append(self.right()) return gridList # deepcopy多維列表的復(fù)制,防止指針賦值將原列表改變 # move只能移動行或列,即row和col必有一個為0 #對當(dāng)前狀態(tài)進(jìn)行移動的函數(shù) def move(self, row, col): newStat = copy.deepcopy(self.stat) tmp = self.stat[self.zero[0] + row][self.zero[1] + col] newStat[self.zero[0]][self.zero[1]] = tmp newStat[self.zero[0] + row][self.zero[1] + col] = 0 return newStat def up(self): return self.move(-1, 0) def down(self): return self.move(1, 0) def left(self): return self.move(0, -1) def right(self): return self.move(0, 1) # 判斷狀態(tài)g是否在狀態(tài)集合中,g是對象,gList是對象列表 # 返回的結(jié)果是一個列表,第一個值是真假,如果是真則第二個值是g在gList中的位置索引 def isin(g, gList): gstat = g.stat statList = [] for i in gList: statList.append(i.stat) if (gstat in statList): res = [True, statList.index(gstat)] else: res = [False, 0] return res # 計算逆序數(shù)之和 def N(nums): N=0 for i in range(len(nums)): if(nums[i]!=0): for j in range(i): if(nums[j]>nums[i]): N+=1 return N # 根據(jù)逆序數(shù)之和判斷所給八數(shù)碼是否可解 def judge(src,target): N1=N(src) N2=N(target) if(N1%2==N2%2): return True else: return False # 初始狀態(tài) startStat = [[2, 8, 3], [1, 0, 4], [7, 6, 5]] g = grid(startStat) # 判斷所給的八數(shù)碼受否有解 if(judge(startStat,g.target)!=True): print("所給八數(shù)碼無解,請檢查輸入") exit(1) # visited儲存的是已經(jīng)擴(kuò)展過的節(jié)點 visited = [] time = 0 # 用遞歸的方式進(jìn)行DFS遍歷 def DFSUtil(v, visited): global time #判斷是否達(dá)到深度界限 if (v.G > 4): return time+=1 #判斷是否已經(jīng)找到解 if (v.H == 0): print("found and times", time, "moves:", v.G) v.seeAns() exit(1) #對當(dāng)前節(jié)點進(jìn)行擴(kuò)展 visited.append(v.stat) expandStats = v.expand() w = [] for stat in expandStats: tmpG = grid(stat) tmpG.pre = v tmpG.update() if (stat not in visited): w.append(tmpG) for vadj in w: DFSUtil(vadj, visited) #visited查重只對一條路,不是全局的,每條路開始時都為空 #因為如果全局查重,會導(dǎo)致例如某條路在第100層找到的狀態(tài),在另一條路是第2層找到也會被當(dāng)做重復(fù) #進(jìn)而導(dǎo)致明明可能會找到解的路被放棄 visited.pop() DFSUtil(g, visited) # 如果找到解程序會在中途退出,走到下面這一步證明沒有找到解 print("在當(dāng)前深度下沒有找到解,請嘗試增加搜索深度")
1.3 實驗結(jié)果
以下面這個八數(shù)碼為例,用 DFS 進(jìn)行搜索。
將找出的解從初始狀態(tài)一步一步輸出到解狀態(tài)。
可以看出總共進(jìn)行了 15 次遍歷,在某一條路的第 4 層找到了解。
下面我們來看一看 DFS 的所有 15 次遍歷,以此來更深入的理解 DFS 的原理。稍微對代碼進(jìn)行改動,使其輸出遍歷次數(shù)和當(dāng)前層數(shù)。由于結(jié)果太長,為了方便展示,下面將以樹的形式展示。
上面輸出的解就是按照紅色路線標(biāo)注找到的,從遍歷次數(shù)可以看出 DFS 是一條道走到黑的找法,因為設(shè)置的深度界限是 4,所以每一條路最多找到第 4 層。
1.4 實驗總結(jié)
1、為什么要設(shè)置深度界限?
因為理論上我們只需要一條路就可以找到解,只要不停地向下擴(kuò)展就可以了。而這樣做的缺點是會繞遠(yuǎn)路,也許第一條路找到第 100 層才找到解,但第二條路找兩層就能找到解。從 DFS 的原理出發(fā),我們不難看出這一點。還有一個問題是其狀態(tài)數(shù)太多了,在不設(shè)置深度界限的情況下經(jīng)常出現(xiàn)即使程序的棧滿了依然沒有找到解的情況。所以理論只是理論,在堅持“一條道走到黑”時,很可能因為程序“爆棧”而走到了黑還是沒有找到解。
2、如何進(jìn)行回溯?
在八數(shù)碼問題中,我們回溯的條件只有一個,就是達(dá)到深度界限了。因為在找到解時會退出,找不到時會繼續(xù)向下擴(kuò)展?;厮莸倪^程是先回溯到父節(jié)點,檢查父節(jié)點是否還能擴(kuò)展其他節(jié)點,如果能,就擴(kuò)展新的節(jié)點并繼續(xù)向下搜索,如果不能則遞歸地繼續(xù)向上回溯。
3、出現(xiàn)重復(fù)狀態(tài)怎么解決?
不難想出假如按照下、左、右、上這樣地順序進(jìn)行搜索時,在第三層時就會出現(xiàn)和初始狀態(tài)相同的情況。因為第二層向一個方向移動,第三層會有一個向反方向移動的狀態(tài)也就是回到初始狀態(tài)了。這樣不僅增加了運算量,而且沒有意義,會出現(xiàn)很多冗余步驟。所以我們應(yīng)該設(shè)置一個查重的表,將已經(jīng)遍歷過的狀態(tài)存入這個表中,當(dāng)再次遇到這種情況時我們就跳過。
那么這個查重的表是該對于全局而言呢,還是每條路的查重表是獨立的呢?在經(jīng)過很多測試之后,我發(fā)現(xiàn)這個查重表對每條路獨立是更好的。因為在一條路上出現(xiàn)的狀態(tài)所需要的步數(shù)和另一條需要的步數(shù)不一定相同,也就是說我在第一條路上的第 100 層找到了某個狀態(tài),放入了查重表中,但是這個狀態(tài)可能在另一條路上第 2 層就能找到,或許再下面幾層就能找到解了,可是由于被放進(jìn)了全局查重表中而放棄了這個條路的擴(kuò)展,也損失了更快找到解的機(jī)會。所以一條路一個查重表是好的。
4、由于需要設(shè)置深度界限,每條路都會在深度界限處截至, 而如果所給的八數(shù)碼的最優(yōu)解大于深度界限,就會出現(xiàn)遍歷完所有情況都找不解。而在事先不知道最優(yōu)解的深度的情況下這個深度界限很難確定,設(shè)置大了會增大搜索時間,設(shè)置小了會找不到解。這也是 DFS 的一個缺點。
5、DFS 不一定能找到最優(yōu)解。因為深度界限的原因,找到的解可能在最優(yōu)解和深度界限之間。
以上就是使用python實現(xiàn)深度優(yōu)先遍歷搜索(DFS)的詳細(xì)內(nèi)容,更多關(guān)于python深度優(yōu)先遍歷搜索的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python Opencv任意形狀目標(biāo)檢測并繪制框圖
這篇文章主要為大家詳細(xì)介紹了Python Opencv任意形狀目標(biāo)檢測,并繪制框圖,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-07-07pytorch 預(yù)訓(xùn)練模型讀取修改相關(guān)參數(shù)的填坑問題
這篇文章主要介紹了pytorch 預(yù)訓(xùn)練模型讀取修改相關(guān)參數(shù)的填坑問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06Python Pandas學(xué)習(xí)之Pandas數(shù)據(jù)結(jié)構(gòu)詳解
Pandas中一共有三種數(shù)據(jù)結(jié)構(gòu),分別為:Series、DataFrame和MultiIndex(老版本中叫Panel )。其中Series是一維數(shù)據(jù)結(jié)構(gòu),DataFrame是二維的表格型數(shù)據(jù)結(jié)構(gòu),MultiIndex是三維的數(shù)據(jù)結(jié)構(gòu)。本文將詳細(xì)為大家講解這三個數(shù)據(jù)結(jié)構(gòu),需要的可以參考一下2022-02-02