python實現圖的操作
圖的介紹
下面就通過一個例子來讓大家快速地知道什麼是圖,如下圖所示,
G1 是有向圖,G2 是無向圖,
- 每個數據元素稱爲頂點,
- 在有向圖中,從 V1 到 V3 稱爲一條弧,V3 到 V1 爲另一條弧,V1 稱爲弧尾,V3 稱爲弧頭,
- 在無向圖中,從 V1 到 V3 稱爲一條邊。
- 圖中有 n個頂點,n(n-1)/2條邊的無向圖稱爲無向完全圖,
- 有n(n-1)條弧有向圖稱爲有向完全圖,有很少條邊或圖稱爲稀疏圖,反之稱爲稠密圖。
- 在 G2 無向圖中,類似 V3與 V1、V2 和 V4 之間有邊的互稱爲鄰接點
- 與頂點相關聯的邊數稱爲頂點的度
- 從一個頂點到另一個頂點的頂點序列稱爲路徑,
在有向圖中
- 路徑是有方向的,路徑上邊或弧的數目稱爲路徑的長度,
- 如果一條路徑中的起始頂點跟結束結點相同,那麼稱這個路徑爲環或迴路,
- 不出現重複頂點的路徑稱爲簡單路徑。
在無向圖中,
- 如果一個頂點到另一個頂點有路徑,那麼它們就是連通的,
- 如果圖中的任意兩個頂點都是連通的,那麼這個圖就是連通圖, 無向圖中的極大連通子圖稱爲連通分量
- 如果是有向圖中的任意一對頂點都有路徑,那麼這個圖就是強連通圖,相應的它的極大連通子圖就稱爲強連通分量
- 一個連通圖的一個極小連通子圖,它包含所有頂點,但足以構成一棵樹的 n-1 條邊,加一條邊必定會形成環,這個就稱爲生成樹。
概念介紹
- 無向圖 圖是若干個頂點(Vertices)和邊(Edges)相互連接組成的。邊僅由兩個頂點連接,並且沒有方向的圖稱爲無向圖。
- 有向圖 在有向圖中,邊是單向的:每條邊連接的兩個頂點都是一個有序對,它們的鄰接性是單向的。我們開發過程中碰到的很多場景都是有向圖:比如任務調度的依賴關係,社交網絡的任務關係等等都是天然的有向圖。
- 度 一個頂點的度是指與該頂點相關聯的邊的條數,頂點 v 的度記作 d(v)。
圖的實現
- 表示圖通常有四種方法:數組表示法、鄰接表、十字鏈表和鄰接多重表。
- 鄰接表是圖的一種鏈式存儲結構
- 十字鏈表是有向圖的另一種鏈式存儲結構,
- 鄰接多重表是無向圖的另一種鏈式存儲結構。
這裏主要講解一下鄰接表的表示和實現,
鄰接表中有兩種結點,一種是頭結點,頭結點中存儲一個頂點的數據和指向鏈表中第一個結點,
另一種是表結點,表結點中存儲當前頂點在圖中的位置和指向下一條邊或弧的結點,表頭結點用鏈式或順序結構方式存儲
圖的鄰接表實現
對於有向圖的鄰接表表示形式,圖的邊可以使用字典數據結構來表示。
下面給出無向圖的參考代碼(對於有向圖,請自行修改,這裏不再提示)
參考代碼如下
class Graph(object):
def __init__(self):
#表示圖的點集
self.nodes=[]
#表示圖的邊集
self.edge={}
def insert(self,a,b):
#如果a不在圖的點集中則添加a
if not(a in self.nodes):
self.nodes.append(a)
#爲點集a開闢一個邊集
self.edge[a]=list()
#如果b不在圖的點集中則添加b
if not (b in self.nodes):
self.nodes.append(b)
#爲點集b開闢一個邊集
self.edge[b]=list()
#a連接b
self.edge[a].append(b)
#TODO b連接a,有向圖中,下行註釋掉即可滿足要求
self.edge[b].append(a)
def succ(self,a):
#返回與a連接的點
return self.edge[a]
def show_nodes(self):
#返回圖的點集
return self.nodes
def show_edge(self):
#返回圖的邊集
print(self.edge)
graph=Graph()
graph.insert('0','1')
graph.insert('0','2')
graph.insert('0','3')
graph.insert('1','3')
graph.insert('2','3')
graph.show_edge()
該 graph 儲存形式爲:
{‘0’: [‘1’, ‘2’, ‘3’], ‘1’: [‘0’, ‘3’], ‘2’: [‘0’, ‘3’], ‘3’: [‘0’, ‘1’, ‘2’]}
圖的鄰接矩陣實現
鄰接矩陣表示法,用兩個數組表示,一個一維數組和一個二維數組。
一維數組存儲節點信息,二維數組存儲節點之間的關係。
參考代碼如下
class Graph:
#初始化n*n矩陣,全置爲0
def __init__(self,vertex):
self.vertex=vertex
self.graph=[[0]*vertex for i in range(vertex)]
def insert(self,u,v):
#對存在連接關係的兩個點,在矩陣裏置1代表連接關係,0則無連接關係
#無向圖,連接關係是關於斜對角對稱的
self.graph[u-1][v-1]=1
self.graph[v-1][u-1]=1
def show(self):
#展示圖
for i in self.graph:
for j in i:
print(j,end=' ')
print(' ')
graph = Graph(5)
graph.insert(1, 4)
graph.insert(4, 2)
graph.insert(4, 5)
graph.insert(2, 5)
graph.insert(5, 3)
graph.show()
該 graph 儲存形式爲:
圖的遍歷問題
通常圖的遍歷有兩種:深度優先搜索和廣度優先搜索。
深度優先遍歷
深度優先搜索(DFS) 是樹的先根遍歷的推廣,它的基本思想是:
從根節點出發,沿着左子樹方向進行縱向遍歷,直到找到葉子節點爲止。
然後回溯到前一個節點,進行右子樹節點的遍歷,
直到遍歷完所有可達節點爲止。
參考代碼如下
def dfs(G,s,S=None,res=None):
if S is None:
# 儲存已經訪問節點
S=set()
if res is None:
# 存儲遍歷順序
res=[]
#存儲首個訪問點
res.append(s)
#存儲訪問的首個節點
S.add(s)
# 遍歷首個節點的存在的路徑及相關的節點
for u in G[s]:
#如果路徑中的節點在已經訪問的節點則跳過,否則,添加到已經訪問的節點列表中
if u in S:
continue
S.add(u)
#繼續進行深度遍歷
dfs(G,u,S,res)
return res
G = {'0': ['1', '2'],
'1': ['2', '3'],
'2': ['3', '5'],
'3': ['4'],
'4': [],
'5': []}
print(dfs(G, '0'))
結果及流程示意圖如下
廣度優先搜索
廣度優先搜索(BFS)是樹的按層次遍歷的推廣,它的基本思想是:
首先訪問初始點 vi,並將其標記爲已訪問過,
接着訪問 vi 的所有未被訪問過的鄰接點 vi1,vi2,…, vin,並均標記已訪問過,
然後再按照 vi1,vi2,…, vin 的次序,訪問每一個頂點的所有未被訪問過的鄰接點,並均標記爲已訪問過,
依次類推,直到圖中所有和初始點 vi 有路徑相通的頂點都被訪問過爲止。
參考代碼如下
def bfs(graph, start):
# explored:已經遍歷的節點列表,queue:尋找待遍歷的節點隊列
explored=[]
#初始遍歷的首個節點
queue = [start]
#節點列表增加首個訪問節點
explored.append(start)
while queue:
# v:將要遍歷的某節點
v = queue.pop(0)
# w:節點 v 的鄰居
for w in graph[v]:
# w:如果 w 未被遍歷,則遍歷
if w not in explored:
# 添加 w 節點到已遍歷的節點列表
explored.append(w)
# 添加 w 節點到尋找待遍歷的節點隊列
queue.append(w)
return explored
G = {'0': ['1', '2'],
'1': ['2', '3'],
'2': ['3', '5'],
'3': ['4'],
'4': [],
'5': []}
print(bfs(G, '0'))
結果及流程示意圖如下
圖的最短路徑
最短路徑問題是圖論研究中的一個經典算法問題,旨在尋找圖(由結點和路徑組成的)中兩結點之間的最短路徑。給定一個圖,和一個源頂點 src,找到從 src 到其它所有所有頂點的最短路徑,圖中可能含有負權值的邊。在這裏我們介紹兩種常見的求解最短路徑問題的算法。
- Dijkstra 的算法是一個貪婪算法,時間複雜度是O(VLogV)(使用最小堆)。但是迪傑斯特拉算法在有負權值邊的圖中不適用,Bellman-Ford
適合這樣的圖。在網絡路由中,該算法會被用作距離向量路由算法。 - Bellman-Ford 也比迪傑斯特拉算法更簡單和同時也適用於分佈式系統。但 Bellman-Ford 的時間複雜度是O(VE),這要比迪傑斯特拉算法慢。(V 爲頂點的個數,E 爲邊的個數)。
Dijkstra 算法
Dijkstra 算法使用了廣度優先搜索解決賦權有向圖或者無向圖的單源最短路徑問題,算法最終得到一個最短路徑樹。該算法常用於路由算法或者作爲其他圖算法的一個子模塊。
對下圖進行 dijkstra
注意:Dijkstra 算法不能處理包含負邊的圖!
參考代碼
import heapq
#尋找開始節點到結束節點的最短路徑
def dijkstra(graph, start, end):
heap = [(0, start)] # cost from start node,end node
#存儲訪問過的節點
visited = []
while heap:
(cost, u) = heapq.heappop(heap)
#如果該路徑相關的節點,訪問過,則跳過,否則,添加到訪問節點列表中
if u in visited:
continue
visited.append(u)
#如果訪問到結束的節點,返回cost值
if u == end:
return cost
for v, c in G[u]:
if v in visited:
continue
next = cost + c
heapq.heappush(heap, (next, v))
return (-1, -1)
G = {'0': [['1', 2], ['2', 5]],
'1': [['0', 2], ['3', 3], ['4', 1]],
'2': [['0', 5], ['5', 3]],
'3': [['1', 3]],
'4': [['1', 1], ['5', 3]],
'5': [['2', 3], ['4', 3]]}
#尋找圖G中,節點4到節點2的最短路徑
shortDistance = dijkstra(G, '4', '2')
print(shortDistance)
最短路徑如下
bellman_ford 算法
含負權邊的帶權有向圖的單源最短路問題。(不能處理帶負權邊的無向圖)。
算法僞碼如下:
procedure BellmanFord(list vertices, list edges, vertex source)
// 該實現讀入邊和節點的列表,並向兩個數組(distance 和 predecessor)中寫入最短路徑信息
// 步驟 1:初始化圖
for each vertex v in vertices:
if v is source then distance[v] := 0
else distance[v] := infinity
predecessor[v] := null
// 步驟 2:重複對每一條邊進行鬆弛操作
for i from 1 to size(vertices)-1:
for each edge (u, v) with weight w in edges:
if distance[u] + w < distance[v]:
distance[v] := distance[u] + w
predecessor[v] := u
// 步驟 3:檢查負權環
for each edge (u, v) with weight w in edges:
if distance[u] + w < distance[v]:
error "圖包含了負權環"
演示:從頂點 4 進行 bellman_ford 找最短路徑