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

以上圖為例,簡述 DFS 的過程。首先從根節(jié)點“1”出發(fā),按一定的順序遍歷其子節(jié)點,這里我們假設優(yōu)先遍歷左邊的。所以,在遍歷“1”之后,我們到了節(jié)點“2”,此時“2”仍有子節(jié)點,所以應繼續(xù)向下遍歷,下一個節(jié)點是“3”,然后是“4”。到了“4”之后,沒有子節(jié)點了,說明我們已經(jīng)將這一條路遍歷完了,接著我們應該回溯,應該回到“4”的父節(jié)點,也就是“3”。因為“3”還有一個子節(jié)點“5”沒有遍歷,所以下一個我們應該遍歷的是“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 在遍歷樹的應用后,我們將其應用于八數(shù)碼問題的解決。八數(shù)碼問題也稱為九宮問題。在 3×3 的棋盤,擺有八個棋子,每個棋子上標有 1 至 8 的某一數(shù)字,不同棋子上標的數(shù)字不相同。棋盤上還有一個空格,與空格相鄰的棋子可以移到空格中。要求解決的問題是:給出一個初始狀態(tài)和一個目標狀態(tài),找出一種從初始轉(zhuǎn)變成目標狀態(tài)的移動棋子步數(shù)最少的移動步驟。所謂問題的一個狀態(tài)就是棋子在棋盤上的一種擺法。棋子移動后,狀態(tài)就會發(fā)生改變。解八數(shù)碼問題實際上就是找出從初始狀態(tài)到達目標狀態(tài)所經(jīng)過的一系列中間過渡狀態(tài)。
上面說的 DFS 遍歷的樹是已經(jīng)存在的,我們只需要按照規(guī)定的遍歷方法就能完成遍歷,而對于八數(shù)碼問題,沒有已經(jīng)存在的路徑供我們遍歷,需要我們從初始狀態(tài)向下延伸(也就是上下左右移動)才能構(gòu)造出類似的樹。

以上圖為例。在使用 DFS 進行搜索時,每個狀態(tài)都會按照一定的順序進行上下左右移動(在上圖中是下、左、右、上的順序),一次移動后會產(chǎn)生一個新的狀態(tài),然后以新狀態(tài)為起點繼續(xù)按約定的順序(例如先向下)移動。終止的條件是找到解或者達到深度界限。那么如果按照圖中下、左、右、上的順序搜索后的結(jié)果將會是最左邊的一條路一直是優(yōu)先向下移動,如果不能向下則依次會是左、右、上的一種。
1.2 實驗代碼
import copy
# 棋盤的類,實現(xiàn)移動和擴展狀態(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是和目標狀態(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)
#以三行三列的形式輸出當前狀態(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)
#對當前狀態(tài)進行擴展,也就是上下左右移動,返回的列表中是狀態(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多維列表的復制,防止指針賦值將原列表改變
# move只能移動行或列,即row和col必有一個為0
#對當前狀態(tài)進行移動的函數(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)擴展過的節(jié)點
visited = []
time = 0
# 用遞歸的方式進行DFS遍歷
def DFSUtil(v, visited):
global time
#判斷是否達到深度界限
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)
#對當前節(jié)點進行擴展
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查重只對一條路,不是全局的,每條路開始時都為空
#因為如果全局查重,會導致例如某條路在第100層找到的狀態(tài),在另一條路是第2層找到也會被當做重復
#進而導致明明可能會找到解的路被放棄
visited.pop()
DFSUtil(g, visited)
# 如果找到解程序會在中途退出,走到下面這一步證明沒有找到解
print("在當前深度下沒有找到解,請嘗試增加搜索深度")1.3 實驗結(jié)果
以下面這個八數(shù)碼為例,用 DFS 進行搜索。

將找出的解從初始狀態(tài)一步一步輸出到解狀態(tài)。

可以看出總共進行了 15 次遍歷,在某一條路的第 4 層找到了解。
下面我們來看一看 DFS 的所有 15 次遍歷,以此來更深入的理解 DFS 的原理。稍微對代碼進行改動,使其輸出遍歷次數(shù)和當前層數(shù)。由于結(jié)果太長,為了方便展示,下面將以樹的形式展示。

上面輸出的解就是按照紅色路線標注找到的,從遍歷次數(shù)可以看出 DFS 是一條道走到黑的找法,因為設置的深度界限是 4,所以每一條路最多找到第 4 層。
1.4 實驗總結(jié)
1、為什么要設置深度界限?
因為理論上我們只需要一條路就可以找到解,只要不停地向下擴展就可以了。而這樣做的缺點是會繞遠路,也許第一條路找到第 100 層才找到解,但第二條路找兩層就能找到解。從 DFS 的原理出發(fā),我們不難看出這一點。還有一個問題是其狀態(tài)數(shù)太多了,在不設置深度界限的情況下經(jīng)常出現(xiàn)即使程序的棧滿了依然沒有找到解的情況。所以理論只是理論,在堅持“一條道走到黑”時,很可能因為程序“爆棧”而走到了黑還是沒有找到解。
2、如何進行回溯?
在八數(shù)碼問題中,我們回溯的條件只有一個,就是達到深度界限了。因為在找到解時會退出,找不到時會繼續(xù)向下擴展?;厮莸倪^程是先回溯到父節(jié)點,檢查父節(jié)點是否還能擴展其他節(jié)點,如果能,就擴展新的節(jié)點并繼續(xù)向下搜索,如果不能則遞歸地繼續(xù)向上回溯。
3、出現(xiàn)重復狀態(tài)怎么解決?
不難想出假如按照下、左、右、上這樣地順序進行搜索時,在第三層時就會出現(xiàn)和初始狀態(tài)相同的情況。因為第二層向一個方向移動,第三層會有一個向反方向移動的狀態(tài)也就是回到初始狀態(tài)了。這樣不僅增加了運算量,而且沒有意義,會出現(xiàn)很多冗余步驟。所以我們應該設置一個查重的表,將已經(jīng)遍歷過的狀態(tài)存入這個表中,當再次遇到這種情況時我們就跳過。
那么這個查重的表是該對于全局而言呢,還是每條路的查重表是獨立的呢?在經(jīng)過很多測試之后,我發(fā)現(xiàn)這個查重表對每條路獨立是更好的。因為在一條路上出現(xiàn)的狀態(tài)所需要的步數(shù)和另一條需要的步數(shù)不一定相同,也就是說我在第一條路上的第 100 層找到了某個狀態(tài),放入了查重表中,但是這個狀態(tài)可能在另一條路上第 2 層就能找到,或許再下面幾層就能找到解了,可是由于被放進了全局查重表中而放棄了這個條路的擴展,也損失了更快找到解的機會。所以一條路一個查重表是好的。
4、由于需要設置深度界限,每條路都會在深度界限處截至, 而如果所給的八數(shù)碼的最優(yōu)解大于深度界限,就會出現(xiàn)遍歷完所有情況都找不解。而在事先不知道最優(yōu)解的深度的情況下這個深度界限很難確定,設置大了會增大搜索時間,設置小了會找不到解。這也是 DFS 的一個缺點。
5、DFS 不一定能找到最優(yōu)解。因為深度界限的原因,找到的解可能在最優(yōu)解和深度界限之間。
以上就是使用python實現(xiàn)深度優(yōu)先遍歷搜索(DFS)的詳細內(nèi)容,更多關(guān)于python深度優(yōu)先遍歷搜索的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
pytorch 預訓練模型讀取修改相關(guān)參數(shù)的填坑問題
這篇文章主要介紹了pytorch 預訓練模型讀取修改相關(guān)參數(shù)的填坑問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06
Python Pandas學習之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)。本文將詳細為大家講解這三個數(shù)據(jù)結(jié)構(gòu),需要的可以參考一下2022-02-02

