图(一)| BFS与DFS算法 - Python实现
文章目錄
- 一. 基礎概念:
- * 圖的三種表示方法:
- 1. 鄰接矩陣:
- 2. 關聯矩陣:
- 3. 鄰接表:
- 二. 廣度優先搜索BFS:
- 1. 實現棧和隊列:
- 2. 實現鄰接表描述的圖:
- 4. 實現廣度優先搜索:
- 5. 廣度優先搜索的簡單應用:
- 三. 深度優先搜索DFS:
- 四. 所有代碼:
一. 基礎概念:
圖的數學表述為 G=(V,E)G=(V,E)G=(V,E),即圖是由一組頂點和一組邊構成,例如:
相關概念如下:
- 相鄰節點;
- 度:相鄰節點的個數;
- 路徑:從某個節點到另一個節點的連續序列;
- 簡單路徑:無重復節點的路徑;
- 無環的;
- 連通的:每兩個節點之間都存在路徑;
- 無向的;
- 有向的;
- 加權的;
- 未加權的;
- 強連通的:對于有向圖,每兩個頂點之間都存在雙向的路徑;
例如上面的例子中展現的就是一張無環的、無向的、未加權的、連通的圖,這樣的圖里任意兩個節點間都存在簡單路徑。
* 圖的三種表示方法:
1. 鄰接矩陣:
array[i][j]={1,i與j為相鄰頂點;0,i與j不相鄰;array[i][j] = \begin{cases} 1, & i與j為相鄰頂點; \\ 0, & i與j不相鄰; \end{cases}array[i][j]={1,0,?i與j為相鄰頂點;i與j不相鄰;?
容易得到對于鄰接矩陣MMM,MT=MM^T = MMT=M。
2. 關聯矩陣:
array[v][e]={1,v是e的入射點;0,v不是e的入射點;array[v][e] = \begin{cases} 1, & v是e的入射點; \\ 0, & v不是e的入射點; \end{cases}array[v][e]={1,0,?v是e的入射點;v不是e的入射點;?
對于有向圖可稍作修改,例如
3. 鄰接表:
鄰接表最簡單的描述方式是使用字典,以某頂點為鍵,以該頂點的相鄰頂點為值即可。例如最開始的例子中的圖可以表示為一個Python字典:
{A:[B,C,D],B:[A,E,F],C:[A,G],D:[A,H,I,J], E:[B,K,L],F:[B],G:[C],H:[D,M],I:[D],J:[D],K:[E],L:[E], M:[H] }后面我們將采用這種方式實現圖,進而使用BFS與DFS算法遍歷圖中的節點。
二. 廣度優先搜索BFS:
廣度優先搜索是一種對圖進行遍歷的算法,其遍歷思想是“先寬后深”,優先訪問同一層的節點;而深度優先搜索的遍歷思想則是“先深后寬”,從指定頂點開始,沿著某條路徑直到這條路徑的最后一個節點,再原路退回,探索下一條路徑。
對于這兩種算法,我們其實只需要將隊列應用到BFS中、將棧應用到DFS中,即可非常相似的實現兩種算法。這一點我們后續可以更清楚的看到。那么首先讓我們實現簡單的棧和隊列:
1. 實現棧和隊列:
棧和隊列的構建都很簡單,我們使用Python提供的列表存儲數據,然后遵守相應的“先進后出”、“先進先出”原則定義入棧/出棧、入隊/出隊的方法即可。最后,我們需要一個方法動態獲取棧/隊列的長度,將size定義為方法而不是屬性可以簡化代碼、避免手動更新size屬性。
代碼如下:
class MyQueue(object):'''構建隊列'''def __init__(self):self.myQueue = []def push(self,item):self.myQueue.append(item)return self.myQueuedef pop(self):return self.myQueue.pop(0)def size(self):'''將隊列的大小動態定義為方法,其它方法中無需對其進行顯示更新'''return len(self.myQueue)class MyStack(object):'''構建棧'''def __init__(self):self.myStack = []def push(self,item):self.myStack.append(item)return self.myStackdef pop(self):return self.myStack.pop(len(self.myStack) - 1)def size(self):return len(self.myStack)2. 實現鄰接表描述的圖:
我們在構建鄰接表描述的圖時,主要任務就是構建一個之前提到過的字典,形如{節點:相鄰節點},其中相鄰節點使用列表存儲。另外為了后續的方便,我們另外構造一個列表存儲圖中所有的節點,技巧是每次更新圖時使用列表轉集合再轉列表進行降重。最后,我們添加了一個方法,返回描述我們表示的圖的字符串,便于檢查。
代碼如下:
class MyGraph(object):'''構建圖'''def __init__(self):self.vertexName = []self.myGraph = {}def push(self,v_first,v_next):self.vertexName.append(v_first)self.vertexName = list(set(self.vertexName)) #節點名構成的列表是無序的if self.myGraph.get(v_first) == None:self.myGraph[v_first] = []self.myGraph[v_first].append(v_next)return self.myGraphdef get(self,v):return self.myGraph[v] def view(self): '''以便于檢查的形式輸出整個圖的結構'''view = ''for v in self.vertexName:view_now = '{} ->'.format(v)for v_next in self.myGraph[v]:view_now += str(v_next) + ' 'view += view_now + '\n'return view另外,為了避免重復構造同一結構的圖,我們使用json庫保存構造好的圖,具體來說是保存構造好的字典與節點列表,所以我們定義兩個常用的函數,一個用于保存數據、一個用于加載數據:
def load(path):'''加載數據'''with open(path,'r') as f_obj:return json.loads(f_obj.read())def save(data,path):'''保存數據'''with open(path,'w') as f_obj:f_obj.write(json.dumps(data))有了這些工具,我們就可以實現用鄰接表表述的圖了。為了方便輸入,我們用一個新函數簡化構造過程:
def get_myGraph(path='myGraph.json'):'''構建鄰接表表示的實際的圖'''myGraph = MyGraph()while True:data = '[' + input("plz input data as data:list:") + ']' #將輸入格式化為JSON字符串if data == '[over]':break #輸入字符床over即可結束輸入工作print(data)input()data = json.loads(data)for i in range(len(data[1])):myGraph.push(data[0],data[1][i])print(myGraph.view())save([myGraph.vertexName,myGraph.myGraph],path)return myGraph.vertexName,myGraph.json #返回字典結構的圖和列表結構的所有節點有了這個函數,我們只需要按照規定的形式輸入我們想要抽象表示的圖,即可將其生成的字典與節點列表存儲到JSON文件中,并且在我們想使用的時候也可以很方便的將其加載。
下面,我們用鄰接表表示最開始給出的示例,得到的字典、節點列表以及圖的字符串描述分別如下:
{"A": ["B", "C", "D"], "B": ["A", "E", "F"], "C": ["A", "G"], "D": ["A", "H", "I", "J"], "E": ["B", "K", "L"], "F": ["B"], "G": ["C"], "H": ["D", "M"], "I": ["D"], "J": ["D"], "K": ["E"], "L": ["E"], "M": ["H"]} ["E", "B", "G", "C", "J", "F", "K", "M", "D", "H", "I", "L", "A"] E ->B K L B ->A E F G ->C C ->A G J ->D F ->B K ->E M ->H D ->A H I J H ->D M I ->D L ->E A ->B C D經檢查無誤。
4. 實現廣度優先搜索:
我們使用三種“顏色”表示節點的不同狀態:
- 所有節點的原始狀態都是white;
- 當某個節點被首次發現時,其狀態轉換為gray;
- 當某個節點的所有相鄰節點都被發現后,其狀態轉換為black;
我們主要關注的是前兩個狀態,而black狀態可以用于檢查,也可以應用在拓撲排序中。
廣度優先搜索的基本過程為選定一個初始節點,獲取其相鄰節點并將初始節點的狀態變為gray,然后將初始節點的相鄰節點添加到隊列中,這樣初始節點的狀態又被轉換為black。
此后,我們只需不斷彈出隊列中的當前節點,獲取當前節點的相鄰節點,將其中狀態為white的節點添加到隊列中并將其狀態改變為gray,最后改變當前節點狀態為black即可。通過white-gray兩個狀態的轉換與檢查,我們可以避免多次訪問同一個節點,而通過隊列的添加與彈出,我們實現了廣度優先的搜索思路。此后實現的深度優先搜索只需將隊列換成棧即可,其它思路一點都沒有變化。
可以看到,為了避免某個節點多次被訪問(多次入隊),我們需要標記其狀態,所以我們先利用一個函數初始化所有節點的狀態:
def get_colorDict(vertexName):'''構造后續使用的節點-狀態字典'''colorDict = {}for vertex in vertexName:colorDict[vertex] = 'white' #將所有節點的狀態初始化未white,意味未被發現return colorDict有了上述工具后我們就可以進行廣度優先搜索:
def bfs_demo(v_first,path='myGraph.json'):'''按照廣度優先方法訪問圖中節點'''[vertex,graph] = load(path)colorDict = get_colorDict(vertex)queue = MyQueue()visitSequence = []#處理根節點:colorDict[v_first] = 'gray'visitSequence.append(v_first)for vertex in graph[v_first]:queue.push(vertex)colorDict[vertex] = 'gray'colorDict[v_first] = 'black'#開始廣度優先搜索while queue.size() != 0:vertex_now = queue.pop()visitSequence.append(vertex_now)for vertex in graph[vertex_now]:if colorDict[vertex] == 'white':queue.push(vertex)colorDict[vertex] = 'gray'colorDict[vertex_now] = 'black' return visitSequence我們將需要的圖、節點列表、狀態字典、隊列一一初始化之后,還需要初始化一個訪問順序列表用來檢查我們的訪問時候滿足廣度優先的條件。
當我們將某個節點的狀態轉換為gray的時候,我們訪問了該節點,所以將其添加到訪問順序列表中。在單獨處理完根節點后,我們在隊列上進行循環,檢查尚未訪問的節點并將其入隊,直到隊列為空。最后我們返回訪問順序列表:
當將A作為根節點時:
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M']當將L作為根節點時:
['L', 'E', 'B', 'K', 'A', 'F', 'C', 'D','G', 'H', 'I', 'J', 'M']可以看到,訪問時是一層一層訪問的。
5. 廣度優先搜索的簡單應用:
我們可以通過廣度優先搜索實現許多應用。此處給出一個簡單的例子:應用廣度優先搜索獲得圖中所有節點到某個指定節點的最短路徑:
def get_recallDict(vertexName):recallDict = {}for vertex in vertexName:recallDict[vertex] = Nonereturn recallDictdef bfs(v_first,path='myGraph.json'):'''使用廣度優先方法遍歷指定節點到其它所有節點的最短路徑'''[vertexName,graph] = load(path)colorDict = get_colorDict(vertexName)queue = MyQueue()recallDict = get_recallDict(vertexName)#處理根節點colorDict[v_first] = 'gray'recallDict[v_first] = v_first for vertex in graph[v_first]:queue.push(vertex)colorDict[vertex] = 'gray'recallDict[vertex] = v_firstcolorDict[v_first] = 'black'#廣度優先搜索并記錄發現的節點的前節點while queue.size() != 0:vertex_now = queue.pop()for vertex in graph[vertex_now]:if colorDict[vertex] == 'white':colorDict[vertex] = 'gray'recallDict[vertex] = vertex_nowqueue.push(vertex)colorDict[vertex_now] = 'black'pathDict = {}for vertex in vertexName:path = '{} -> '.format(vertex)v_next = recallDict[vertex]while v_next != v_first:path += '{} -> '.format(v_next)v_next = recallDict[v_next]path += '{}'.format(v_first)pathDict[vertex] = pathprint(path)return pathDict為此,我們首先通過函數初始化了一個回溯字典,該字典記錄我們的訪問過程中某個節點的前一個節點。有了這樣的回溯字典,我們可以得到整個路徑。
之后,我們只需要在初始化的時候多初始化一個回溯字典,即可開始我們的搜索。過程相似,只是在將某個節點狀態轉換為gray的時候,我們在回溯字典中記錄該節點的前節點。
訪問完成后,我們即可進行回溯。首先定義一個路徑字典,用于存儲某個節點到指定節點的路徑,最后我們也會返回路徑字典,便于針對性的查找。但是為了便于檢查,我們一次性將所有的結果視圖化,將視圖化的結果(字符串)打印出來。
結果如下:
以A為指定節點:
以L為指定節點:
E -> L B -> E -> L G -> C -> A -> B -> E -> L C -> A -> B -> E -> L J -> D -> A -> B -> E -> L F -> B -> E -> L K -> E -> L M -> H -> D -> A -> B -> E -> L D -> A -> B -> E -> L H -> D -> A -> B -> E -> L I -> D -> A -> B -> E -> L L -> L A -> B -> E -> L三. 深度優先搜索DFS:
對于深度優先搜索,我們只需要將隊列改為棧即可,所以此處不進行代碼展示,想了解可以直接看文章的第四部分。
下面是一些結果:
以A為根節點的訪問順序:
['A', 'D', 'J', 'I', 'H', 'M', 'C', 'G','B', 'F', 'E', 'L', 'K']以L為根節點的訪問順序:
['L', 'E', 'K', 'B', 'F', 'A', 'D', 'J', 'I', 'H', 'M', 'C', 'G']同樣的,我們分別以A和L作為指定節點,可以得到路徑組:
E -> B -> A B -> A G -> C -> A C -> A J -> D -> A F -> B -> A K -> E -> B -> A M -> H -> D -> A D -> A H -> D -> A I -> D -> A L -> E -> B -> A A -> A E -> L B -> E -> L G -> C -> A -> B -> E -> L C -> A -> B -> E -> L J -> D -> A -> B -> E -> L F -> B -> E -> L K -> E -> L M -> H -> D -> A -> B -> E -> L D -> A -> B -> E -> L H -> D -> A -> B -> E -> L I -> D -> A -> B -> E -> L L -> L A -> B -> E -> L我們選取的圖比較簡答,但是方便理解BFS與DFS的訪問順序,可以選取更復雜的圖進行測試,看看二者得到的路徑有什么不同。需要注意的是,我們的圖可以是有環的,但這樣的代碼需要保證圖是連通的(強連通的),且目前只適用于無權圖,后續我們將討論更加實用的有權圖。對于連通性的要求,我們只需針對特定的圖稍加修改即可。
四. 所有代碼:
import jsonclass MyQueue(object):'''構建隊列'''def __init__(self):self.myQueue = []def push(self,item):self.myQueue.append(item)return self.myQueuedef pop(self):return self.myQueue.pop(0)def size(self):'''將隊列的大小動態定義為方法,其它方法中無需對其進行顯示更新'''return len(self.myQueue)class MyStack(object):'''構建棧'''def __init__(self):self.myStack = []def push(self,item):self.myStack.append(item)return self.myStackdef pop(self):return self.myStack.pop(len(self.myStack) - 1)def size(self):return len(self.myStack)class MyGraph(object):'''構建圖'''def __init__(self):self.vertexName = []self.myGraph = {}def push(self,v_first,v_next):self.vertexName.append(v_first)self.vertexName = list(set(self.vertexName)) #節點名構成的列表是無序的if self.myGraph.get(v_first) == None:self.myGraph[v_first] = []self.myGraph[v_first].append(v_next)return self.myGraphdef get(self,v):return self.myGraph[v] def view(self): '''以便于檢查的形式輸出整個圖的結構'''view = ''for v in self.vertexName:view_now = '{} ->'.format(v)for v_next in self.myGraph[v]:view_now += str(v_next) + ' 'view += view_now + '\n'return viewdef load(path):'''加載數據'''with open(path,'r') as f_obj:return json.loads(f_obj.read())def save(data,path):'''保存數據'''with open(path,'w') as f_obj:f_obj.write(json.dumps(data))def get_myGraph(path='myGraph.json'):'''構建鄰接表表示的實際的圖'''myGraph = MyGraph()while True:data = '[' + input("plz input data as data:list:") + ']' #將輸入格式化為JSON字符串if data == '[over]':break #輸入字符床over即可結束輸入工作print(data)input()data = json.loads(data)for i in range(len(data[1])):myGraph.push(data[0],data[1][i])print(myGraph.view())save([myGraph.vertexName,myGraph.myGraph],path)return myGraph.vertexName,myGraph.json #返回字典結構的圖和列表結構的所有節點def get_colorDict(vertexName):'''構造后續使用的節點-狀態字典'''colorDict = {}for vertex in vertexName:colorDict[vertex] = 'white' #將所有節點的狀態初始化未white,意味未被發現return colorDictdef bfs_demo(v_first,path='myGraph.json'):'''按照廣度優先方法訪問圖中節點'''[vertex,graph] = load(path)colorDict = get_colorDict(vertex)queue = MyQueue()visitSequence = []#處理根節點:colorDict[v_first] = 'gray'visitSequence.append(v_first)for vertex in graph[v_first]:queue.push(vertex)colorDict[vertex] = 'gray'colorDict[v_first] = 'black'#開始廣度優先搜索while queue.size() != 0:vertex_now = queue.pop()visitSequence.append(vertex_now)for vertex in graph[vertex_now]:if colorDict[vertex] == 'white':queue.push(vertex)colorDict[vertex] = 'gray'colorDict[vertex_now] = 'black' return visitSequencedef get_recallDict(vertexName):recallDict = {}for vertex in vertexName:recallDict[vertex] = Nonereturn recallDictdef bfs(v_first,path='myGraph.json'):'''使用廣度優先方法遍歷指定節點到其它所有節點的最短路徑'''[vertexName,graph] = load(path)colorDict = get_colorDict(vertexName)queue = MyQueue()recallDict = get_recallDict(vertexName)#處理根節點colorDict[v_first] = 'gray'recallDict[v_first] = v_first for vertex in graph[v_first]:queue.push(vertex)colorDict[vertex] = 'gray'recallDict[vertex] = v_firstcolorDict[v_first] = 'black'#廣度優先搜索并記錄發現的節點的前節點while queue.size() != 0:vertex_now = queue.pop()for vertex in graph[vertex_now]:if colorDict[vertex] == 'white':colorDict[vertex] = 'gray'recallDict[vertex] = vertex_nowqueue.push(vertex)colorDict[vertex_now] = 'black'pathDict = {}for vertex in vertexName:path = '{} -> '.format(vertex)v_next = recallDict[vertex]while v_next != v_first:path += '{} -> '.format(v_next)v_next = recallDict[v_next]path += '{}'.format(v_first)pathDict[vertex] = pathprint(path)return pathDictdef dfs_demo(v_first,path='myGraph.json'):'''按照深度優先方法訪問圖中節點'''[vertex,graph] = load(path)colorDict = get_colorDict(vertex)stack = MyStack()visitSequence = []#處理根節點:colorDict[v_first] = 'gray'visitSequence.append(v_first)for vertex in graph[v_first]:stack.push(vertex)colorDict[vertex] = 'gray'colorDict[v_first] = 'black'#開始深度優先搜索while stack.size() != 0:vertex_now = stack.pop()visitSequence.append(vertex_now)for vertex in graph[vertex_now]:if colorDict[vertex] == 'white':stack.push(vertex)colorDict[vertex] = 'gray'colorDict[vertex_now] = 'black' return visitSequencedef dfs(v_first,path='myGraph.json'):'''按照深度優先方法遍歷指定節點到其它所有節點的最短路徑'''[vertexName,graph] = load(path)colorDict = get_colorDict(vertexName)stack = MyStack()recallDict = get_recallDict(vertexName)#處理根節點colorDict[v_first] = 'gray'recallDict[v_first] = v_first for vertex in graph[v_first]:stack.push(vertex)colorDict[vertex] = 'gray'recallDict[vertex] = v_firstcolorDict[v_first] = 'black'#深度優先搜索并記錄發現的節點的前節點while stack.size() != 0:vertex_now = stack.pop()for vertex in graph[vertex_now]:if colorDict[vertex] == 'white':colorDict[vertex] = 'gray'recallDict[vertex] = vertex_nowstack.push(vertex)colorDict[vertex_now] = 'black'pathDict = {}for vertex in vertexName:path = '{} -> '.format(vertex)v_next = recallDict[vertex]while v_next != v_first:path += '{} -> '.format(v_next)v_next = recallDict[v_next]path += '{}'.format(v_first)pathDict[vertex] = pathprint(path)return pathDictif __name__ == '__main__':pass總結
以上是生活随笔為你收集整理的图(一)| BFS与DFS算法 - Python实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Centos中查找文件、目录、内容
- 下一篇: cmake 判断操作系统平台