使用python實(shí)現(xiàn)深度優(yōu)先遍歷搜索(DFS)的示例代碼
1.1 算法介紹
深度優(yōu)先搜索算法(Depth-First-Search,DFS)是一種用于遍歷或搜索樹或圖的算法。沿著樹的深度遍歷樹的節(jié)點(diǎn),盡可能深的搜索樹的分支。當(dāng)節(jié)點(diǎn) v 的所在邊都己被探尋過,搜索將回溯到發(fā)現(xiàn)節(jié)點(diǎn) v 的那條邊的起始節(jié)點(diǎn)。這一過程一直進(jìn)行到已發(fā)現(xiàn)從源節(jié)點(diǎn)可達(dá)的所有節(jié)點(diǎn)為止。如果還存在未被發(fā)現(xiàn)的節(jié)點(diǎn),則選擇其中一個(gè)作為源節(jié)點(diǎn)并重復(fù)以上過程,整個(gè)進(jìn)程反復(fù)進(jìn)行直到所有節(jié)點(diǎn)都被訪問為止。屬于盲目搜索。

以上圖為例,簡述 DFS 的過程。首先從根節(jié)點(diǎn)“1”出發(fā),按一定的順序遍歷其子節(jié)點(diǎn),這里我們假設(shè)優(yōu)先遍歷左邊的。所以,在遍歷“1”之后,我們到了節(jié)點(diǎn)“2”,此時(shí)“2”仍有子節(jié)點(diǎn),所以應(yīng)繼續(xù)向下遍歷,下一個(gè)節(jié)點(diǎn)是“3”,然后是“4”。到了“4”之后,沒有子節(jié)點(diǎn)了,說明我們已經(jīng)將這一條路遍歷完了,接著我們應(yīng)該回溯,應(yīng)該回到“4”的父節(jié)點(diǎn),也就是“3”。因?yàn)?ldquo;3”還有一個(gè)子節(jié)點(diǎn)“5”沒有遍歷,所以下一個(gè)我們應(yīng)該遍歷的是“5”。遍歷完“5”之后又發(fā)現(xiàn)一條路到頭了,再次回溯依然回溯到其父節(jié)點(diǎn)“3”,此時(shí)“3”的所有子節(jié)點(diǎn)都已經(jīng)遍歷完了,因該接著回溯到“3”的父節(jié)點(diǎn)“2”,然后檢查“2”是否有沒有遍歷完的子節(jié)點(diǎn)。按照這樣的規(guī)則,完成所有節(jié)點(diǎn)的遍歷。最終得到的遍歷順序是“1-2-3-4-5-6-7-8-9-10-11-12”
在介紹了 DFS 在遍歷樹的應(yīng)用后,我們將其應(yīng)用于八數(shù)碼問題的解決。八數(shù)碼問題也稱為九宮問題。在 3×3 的棋盤,擺有八個(gè)棋子,每個(gè)棋子上標(biāo)有 1 至 8 的某一數(shù)字,不同棋子上標(biāo)的數(shù)字不相同。棋盤上還有一個(gè)空格,與空格相鄰的棋子可以移到空格中。要求解決的問題是:給出一個(gè)初始狀態(tài)和一個(gè)目標(biāo)狀態(tài),找出一種從初始轉(zhuǎn)變成目標(biāo)狀態(tài)的移動棋子步數(shù)最少的移動步驟。所謂問題的一個(gè)狀態(tài)就是棋子在棋盤上的一種擺法。棋子移動后,狀態(tài)就會發(fā)生改變。解八數(shù)碼問題實(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)行搜索時(shí),每個(gè)狀態(tài)都會按照一定的順序進(jìn)行上下左右移動(在上圖中是下、左、右、上的順序),一次移動后會產(chǎn)生一個(gè)新的狀態(tài),然后以新狀態(tài)為起點(diǎn)繼續(xù)按約定的順序(例如先向下)移動。終止的條件是找到解或者達(dá)到深度界限。那么如果按照圖中下、左、右、上的順序搜索后的結(jié)果將會是最左邊的一條路一直是優(yōu)先向下移動,如果不能向下則依次會是左、右、上的一種。
1.2 實(shí)驗(yàn)代碼
import copy
# 棋盤的類,實(shí)現(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必有一個(gè)為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è)列表,第一個(gè)值是真假,如果是真則第二個(gè)值是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
# 計(jì)算逆序數(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é)點(diǎn)
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é)點(diǎn)進(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查重只對一條路,不是全局的,每條路開始時(shí)都為空
#因?yàn)槿绻植橹兀瑫?dǎo)致例如某條路在第100層找到的狀態(tài),在另一條路是第2層找到也會被當(dāng)做重復(fù)
#進(jìn)而導(dǎo)致明明可能會找到解的路被放棄
visited.pop()
DFSUtil(g, visited)
# 如果找到解程序會在中途退出,走到下面這一步證明沒有找到解
print("在當(dāng)前深度下沒有找到解,請嘗試增加搜索深度")1.3 實(shí)驗(yàn)結(jié)果
以下面這個(gè)八數(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 是一條道走到黑的找法,因?yàn)樵O(shè)置的深度界限是 4,所以每一條路最多找到第 4 層。
1.4 實(shí)驗(yàn)總結(jié)
1、為什么要設(shè)置深度界限?
因?yàn)槔碚撋衔覀冎恍枰粭l路就可以找到解,只要不停地向下擴(kuò)展就可以了。而這樣做的缺點(diǎn)是會繞遠(yuǎn)路,也許第一條路找到第 100 層才找到解,但第二條路找兩層就能找到解。從 DFS 的原理出發(fā),我們不難看出這一點(diǎn)。還有一個(gè)問題是其狀態(tài)數(shù)太多了,在不設(shè)置深度界限的情況下經(jīng)常出現(xiàn)即使程序的棧滿了依然沒有找到解的情況。所以理論只是理論,在堅(jiān)持“一條道走到黑”時(shí),很可能因?yàn)槌绦?ldquo;爆棧”而走到了黑還是沒有找到解。
2、如何進(jìn)行回溯?
在八數(shù)碼問題中,我們回溯的條件只有一個(gè),就是達(dá)到深度界限了。因?yàn)樵谡业浇鈺r(shí)會退出,找不到時(shí)會繼續(xù)向下擴(kuò)展?;厮莸倪^程是先回溯到父節(jié)點(diǎn),檢查父節(jié)點(diǎn)是否還能擴(kuò)展其他節(jié)點(diǎn),如果能,就擴(kuò)展新的節(jié)點(diǎn)并繼續(xù)向下搜索,如果不能則遞歸地繼續(xù)向上回溯。
3、出現(xiàn)重復(fù)狀態(tài)怎么解決?
不難想出假如按照下、左、右、上這樣地順序進(jìn)行搜索時(shí),在第三層時(shí)就會出現(xiàn)和初始狀態(tài)相同的情況。因?yàn)榈诙酉蛞粋€(gè)方向移動,第三層會有一個(gè)向反方向移動的狀態(tài)也就是回到初始狀態(tài)了。這樣不僅增加了運(yùn)算量,而且沒有意義,會出現(xiàn)很多冗余步驟。所以我們應(yīng)該設(shè)置一個(gè)查重的表,將已經(jīng)遍歷過的狀態(tài)存入這個(gè)表中,當(dāng)再次遇到這種情況時(shí)我們就跳過。
那么這個(gè)查重的表是該對于全局而言呢,還是每條路的查重表是獨(dú)立的呢?在經(jīng)過很多測試之后,我發(fā)現(xiàn)這個(gè)查重表對每條路獨(dú)立是更好的。因?yàn)樵谝粭l路上出現(xiàn)的狀態(tài)所需要的步數(shù)和另一條需要的步數(shù)不一定相同,也就是說我在第一條路上的第 100 層找到了某個(gè)狀態(tài),放入了查重表中,但是這個(gè)狀態(tài)可能在另一條路上第 2 層就能找到,或許再下面幾層就能找到解了,可是由于被放進(jìn)了全局查重表中而放棄了這個(gè)條路的擴(kuò)展,也損失了更快找到解的機(jī)會。所以一條路一個(gè)查重表是好的。
4、由于需要設(shè)置深度界限,每條路都會在深度界限處截至, 而如果所給的八數(shù)碼的最優(yōu)解大于深度界限,就會出現(xiàn)遍歷完所有情況都找不解。而在事先不知道最優(yōu)解的深度的情況下這個(gè)深度界限很難確定,設(shè)置大了會增大搜索時(shí)間,設(shè)置小了會找不到解。這也是 DFS 的一個(gè)缺點(diǎn)。
5、DFS 不一定能找到最優(yōu)解。因?yàn)樯疃冉缦薜脑?,找到的解可能在最?yōu)解和深度界限之間。
以上就是使用python實(shí)現(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)檢測,并繪制框圖,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07
Python3.5面向?qū)ο笈c繼承圖文實(shí)例詳解
這篇文章主要介紹了Python3.5面向?qū)ο笈c繼承,結(jié)合圖文與實(shí)例形式詳細(xì)分析了Python3.5面向?qū)ο笈c繼承的相關(guān)概念、原理、實(shí)現(xiàn)方法及操作注意事項(xiàng),需要的朋友可以參考下2019-04-04
pytorch 預(yù)訓(xùn)練模型讀取修改相關(guān)參數(shù)的填坑問題
這篇文章主要介紹了pytorch 預(yù)訓(xùn)練模型讀取修改相關(guān)參數(shù)的填坑問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06
Python 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ì)為大家講解這三個(gè)數(shù)據(jù)結(jié)構(gòu),需要的可以參考一下2022-02-02

