使用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)的移動(dòng)棋子步數(shù)最少的移動(dòng)步驟。所謂問題的一個(gè)狀態(tài)就是棋子在棋盤上的一種擺法。棋子移動(dòng)后,狀態(tài)就會(huì)發(fā)生改變。解八數(shù)碼問題實(shí)際上就是找出從初始狀態(tài)到達(dá)目標(biāo)狀態(tài)所經(jīng)過的一系列中間過渡狀態(tài)。
上面說的 DFS 遍歷的樹是已經(jīng)存在的,我們只需要按照規(guī)定的遍歷方法就能完成遍歷,而對(duì)于八數(shù)碼問題,沒有已經(jīng)存在的路徑供我們遍歷,需要我們從初始狀態(tài)向下延伸(也就是上下左右移動(dòng))才能構(gòu)造出類似的樹。

以上圖為例。在使用 DFS 進(jìn)行搜索時(shí),每個(gè)狀態(tài)都會(huì)按照一定的順序進(jìn)行上下左右移動(dòng)(在上圖中是下、左、右、上的順序),一次移動(dòng)后會(huì)產(chǎn)生一個(gè)新的狀態(tài),然后以新狀態(tài)為起點(diǎn)繼續(xù)按約定的順序(例如先向下)移動(dòng)。終止的條件是找到解或者達(dá)到深度界限。那么如果按照?qǐng)D中下、左、右、上的順序搜索后的結(jié)果將會(huì)是最左邊的一條路一直是優(yōu)先向下移動(dòng),如果不能向下則依次會(huì)是左、右、上的一種。
1.2 實(shí)驗(yàn)代碼
import copy
# 棋盤的類,實(shí)現(xiàn)移動(dòng)和擴(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)
# 查看找到的解是如何從頭移動(dòng)的
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)
#對(duì)當(dāng)前狀態(tài)進(jìn)行擴(kuò)展,也就是上下左右移動(dòng),返回的列表中是狀態(tài)的二維列表,不是對(duì)象
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只能移動(dòng)行或列,即row和col必有一個(gè)為0
#對(duì)當(dāng)前狀態(tài)進(jìn)行移動(dòng)的函數(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是對(duì)象,gList是對(duì)象列表
# 返回的結(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ù)碼無解,請(qǐng)檢查輸入")
exit(1)
# visited儲(chǔ)存的是已經(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)
#對(duì)當(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查重只對(duì)一條路,不是全局的,每條路開始時(shí)都為空
#因?yàn)槿绻植橹?,?huì)導(dǎo)致例如某條路在第100層找到的狀態(tài),在另一條路是第2層找到也會(huì)被當(dāng)做重復(fù)
#進(jìn)而導(dǎo)致明明可能會(huì)找到解的路被放棄
visited.pop()
DFSUtil(g, visited)
# 如果找到解程序會(huì)在中途退出,走到下面這一步證明沒有找到解
print("在當(dāng)前深度下沒有找到解,請(qǐng)嘗試增加搜索深度")1.3 實(shí)驗(yàn)結(jié)果
以下面這個(gè)八數(shù)碼為例,用 DFS 進(jìn)行搜索。

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

可以看出總共進(jìn)行了 15 次遍歷,在某一條路的第 4 層找到了解。
下面我們來看一看 DFS 的所有 15 次遍歷,以此來更深入的理解 DFS 的原理。稍微對(duì)代碼進(jìn)行改動(dòng),使其輸出遍歷次數(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)是會(huì)繞遠(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í)會(huì)退出,找不到時(shí)會(huì)繼續(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í)就會(huì)出現(xiàn)和初始狀態(tài)相同的情況。因?yàn)榈诙酉蛞粋€(gè)方向移動(dòng),第三層會(huì)有一個(gè)向反方向移動(dòng)的狀態(tài)也就是回到初始狀態(tài)了。這樣不僅增加了運(yùn)算量,而且沒有意義,會(huì)出現(xiàn)很多冗余步驟。所以我們應(yīng)該設(shè)置一個(gè)查重的表,將已經(jīng)遍歷過的狀態(tài)存入這個(gè)表中,當(dāng)再次遇到這種情況時(shí)我們就跳過。
那么這個(gè)查重的表是該對(duì)于全局而言呢,還是每條路的查重表是獨(dú)立的呢?在經(jīng)過很多測(cè)試之后,我發(fā)現(xiàn)這個(gè)查重表對(duì)每條路獨(dú)立是更好的。因?yàn)樵谝粭l路上出現(xiàn)的狀態(tài)所需要的步數(shù)和另一條需要的步數(shù)不一定相同,也就是說我在第一條路上的第 100 層找到了某個(gè)狀態(tài),放入了查重表中,但是這個(gè)狀態(tài)可能在另一條路上第 2 層就能找到,或許再下面幾層就能找到解了,可是由于被放進(jìn)了全局查重表中而放棄了這個(gè)條路的擴(kuò)展,也損失了更快找到解的機(jī)會(huì)。所以一條路一個(gè)查重表是好的。
4、由于需要設(shè)置深度界限,每條路都會(huì)在深度界限處截至, 而如果所給的八數(shù)碼的最優(yōu)解大于深度界限,就會(huì)出現(xiàn)遍歷完所有情況都找不解。而在事先不知道最優(yōu)解的深度的情況下這個(gè)深度界限很難確定,設(shè)置大了會(huì)增大搜索時(shí)間,設(shè)置小了會(huì)找不到解。這也是 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)先遍歷搜索的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
專家解析Python文件讀寫的理論與實(shí)踐(從基礎(chǔ)概念到工程應(yīng)用)
本文講解Python文件讀寫的基礎(chǔ)與工程實(shí)踐,從文件對(duì)象、打開與關(guān)閉、上下文管理,到文本與二進(jìn)制文件讀寫、文件指針、編碼問題及異常處理,輔以豐富代碼示例與性能、安全提示,通過日志分析工具的實(shí)際項(xiàng)目案例,展示大文件處理、流式讀取、隨機(jī)訪問和編碼容錯(cuò)的應(yīng)用方法2026-01-01
python獲取圖片中兩個(gè)點(diǎn)的具體坐標(biāo)并將圖無損裁剪下來
這篇文章主要為大家詳細(xì)介紹了python如何獲取圖片中兩個(gè)點(diǎn)的具體坐標(biāo)并將圖無損裁剪下來,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2025-11-11
PIL包中Image模塊的convert()函數(shù)的具體使用
這篇文章主要介紹了PIL包中Image模塊的convert()函數(shù)的具體使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
python實(shí)現(xiàn)套接字創(chuàng)建
這篇文章主要為大家介紹了python套接字創(chuàng)建實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
Python操作Neo4j圖數(shù)據(jù)庫的使用完整指南
相比傳統(tǒng)數(shù)據(jù)庫,Neo4j查詢速度快10-100倍,而Python的簡潔語法讓開發(fā)如虎添翼,那么如何用Python連接Neo4j并實(shí)現(xiàn)節(jié)點(diǎn)和關(guān)系的增刪改查,下面小編就來和大家詳細(xì)介紹一下吧2025-05-05
Python使用Beets模塊實(shí)現(xiàn)自動(dòng)整理音樂庫
Beets是一個(gè)功能強(qiáng)大的Python庫,用于處理音樂文件的元數(shù)據(jù),在本文中,我們將探討beets模塊的常見使用方法,感興趣的可以跟隨小編一起學(xué)習(xí)一下2024-03-03

