python无向加权图_图:无向图(Graph)基本方法及Dijkstra算法的实现 [Python]
一般來講,實現圖的過程中需要有兩個自定義的類進行支撐:頂點(Vertex)類,和圖(Graph)類。按照這一架構,Vertex類至少需要包含名稱(或者某個代號、數據)和鄰接頂點兩個參數,前者作為頂點的標識,后者形成頂點和頂點相連的邊,相應地必須有訪問獲取和設定參數的方法加以包裝。Graph類至少需要擁有一個包含所有點的數據結構(列表或者map等),相應地應該有新增頂點、訪問頂點、新增連接邊等方法。當然,為了實現Dijkstra算法(一種基本的最短路徑算法),除了可以在Graph類里增加一個執行Dijkstra算法的方法以外,還需要在Vertex類里增加用于Dijkstra算法的一些參數:某一個頂點距離Dijkstra搜索起點的距離,以及一旦完成Dijkstra搜索需要回溯路徑時,前驅頂點的信息。
在這里記錄用python實現以上基本方法和Dijkstra算法的代碼,因為Python中的現成的數據結構類型便于使用,比如字典dict類,很方便地能夠構造一個類似于map的映射,而且Python的sort方法也特別好用。首先是自定義類頂點(Vertex)的代碼,如上所述,為了滿足BFS算法的執行和回溯的需要,Vertex類一共有四個參數:id(標記)、connections(鄰接頂點字典,鍵值為鄰接頂點,對應值為權重或者邊長)、前驅頂點pre和從起點的距離distance。這二者在Dijkstra算法的執行過程中被賦值,在回溯時需要利用pre的信息。
Vertex類的方法中多數為基本的訪問參數(get)-設定參數(set)的方法對,比如訪問/設定鄰接頂點信息的?add_neighbour(neighbour, weight)?和?get_connection()?,訪問/設定前驅頂點信息的?get_pre()? 和?set_pre(prev)?,以及訪問/設定從起點起距離的?get_distance()? 和?set_distance(dist)?。除此之外重載了字符串化方法,即__str__,這便于print函數和str函數將Vertex類轉化為一個有意義的字符串。
1 classVertex:2 #初始化構造函數,name為字符串,connections字典為
3 #前驅頂點pre,從起點距離distance,在Dijkstra執行賦值后有意義
4 def __init__(self, name):5 self.id =name6 self.pre =None7 self.distance = float('inf')8 self.connections =dict()9
10 #重載字符串化函數,返回字符串
11 def __str__(self):12 return str(self.id) + "connected to:" + str([x.id for x inself.connections])13
14 #增加相鄰頂點,neighbour為Vertex類,weight為浮點型邊權重
15 def add_neighbour(self, neighbour, weight=0):16 self.connections[neighbour] =weight17
18 #獲取頂點id函數
19 defget_id(self):20 returnself.id21
22 #獲取頂點鄰接點的函數,返回鍵值(Vertex)列表
23 defget_connections(self):24 returnself.connections.keys()25
26 #獲取頂點與鄰接點邊權重,傳入Vertex類對象neighbour,返回weight(fl)
27 defget_weight(self, neighbour):28 returnself.connections[neighbour]29
30 #獲取頂點的距離(在BFS執行后使用)
31 defget_distance(self):32 returnself.distance33
34 #獲取前驅頂點(在Dijkstra執行后使用)
35 defget_pre(self):36 returnself.pre37
38 #設定頂點的距離(在Dijkstra執行過程中調用)
39 defset_distance(self, dist):40 self.distance =dist41
42 #設定前驅頂點(在Dijkstra執行過程中調用)
43 defset_pre(self, prev):44 self.pre = prev
圖(Graph)類:擁有兩個參數:頂點字典(頂點id:頂點Vertex類)和頂點數量。頂點數量這個參數似乎沒什么用,除非是增加一個判斷圖是否為空的?isEmpty()?方法和?getSize()?方法可能用得著。
方法中包含新增頂點的?add_vertex(name)?,按照id獲取頂點的?get_vertex(name)?。dict的數據結構給按照id訪問頂點在空間上和時間上都創造了極大的方便,也為此參數中為頂點字典而不用頂點列表。添加邊的方法?add_edge(vertex1, vertex2, weight)?,其中需要調用Vertex類中設定鄰接點的方法。除此之外,Graph類還有一些重載的方法,比如重載contains,這個函數在類似于?3 in list(range(5))?這樣的語句中被調用,比如經常用的,判斷某個元素是否在列表內等。重載迭代器__iter__,類似于?for item in list(range(10)):?這樣的語句中被調用,有助于圖中所有Vertex的遍歷。基于迭代器還可以重載字符串化方法__str__。
最后就是Dijkstra算法。關于這個算法的信息準備記錄在另一篇隨筆中,基本思路是創建一個優先隊列(即距離起始點路程從小到大的隊列),每次查看列表中路程最短的點A,觀察在這個頂點A的鄰接點中,是否有可能因為通過頂點A而使得該鄰接點的路程縮減。每次這樣一輪操作結束以后就從隊列中刪去這個點A,然后把隊列重新排列一遍。在以前課上的學習中,優先隊列使用的是二叉堆(Binary Heap)實現,在這里我直接調用了Python內置的sort函數,雖然計算復雜度不敢保證,但是從后面用的實際例子的效率來說也沒有任何影響。
1 classGraph:2 #無參數構造函數,vertex_dict為映射字典
3 def __init__(self):4 self.vertex_dict =dict()5 self.vertex_num =06
7 #增加頂點函數,傳入新增頂點id
8 defadd_vertex(self, name):9 if name not inself.vertex_dict.keys():10 new_vertex =Vertex(name)11 self.vertex_dict[name] =new_vertex12 self.vertex_num = self.vertex_num + 1
13
14 #增加邊函數, 傳入頂點1名稱、頂點2名稱、權重
15 defadd_edge(self, vertex1, vertex2, weight):16 if vertex1 not inself.vertex_dict:17 self.add_vertex(vertex1)18 if vertex2 not inself.vertex_dict:19 self.add_vertex(vertex2)20 self.vertex_dict[vertex1].add_neighbour(self.vertex_dict[vertex2], weight)21 self.vertex_dict[vertex2].add_neighbour(self.vertex_dict[vertex1], weight)22
23 #按照id檢索頂點函數,傳入id,返回Vertex類
24 defget_vertex(self, name):25 if name inself.vertex_dict.keys():26 returnself.vertex_dict[name]27 else:28 returnNone29
30 #重載contains方法,傳入id,返回bool值
31 def __contains__(self, item):32 return item inself.vertex_dict33
34 #重載迭代器,返回對應迭代器
35 def __iter__(self):36 returniter(self.vertex_dict.values())37
38 #重載字符串化方法,返回字符串
39 def __str__(self):40 o_str =str()41 for item inself:42 o_str = o_str + str(item) + '\n'
43 returno_str44
45 #Dijkstra算法,傳入起點
46 defdijkstra_search(self, start):47 #優先隊列priority
48 priority =list(self.vertex_dict.values())49 #起點距離置零
50 start.set_distance(0)51 #優先隊列重排
52 priority.sort(key=lambda x: x.get_distance(), reverse=True)53 whilepriority:54 #重排標記changed,若存在頂點發生distance變化則標記為True
55 changed =False56 #彈出最高優先頂點current
57 current =priority.pop()58 #遍歷current鄰接頂點
59 for vertex_tmp incurrent.get_connections():60 dist_tmp = current.get_distance() +current.get_weight(vertex_tmp)61 #若發現優勢路徑則更改鄰接頂點的distance和前驅頂點pre
62 if dist_tmp
67 ifchanged:68 priority.sort(key=lambda x: x.get_distance(), reverse=True)
最后,和Dijkstra算法配合還需要一個路徑回溯的方法。本來把回溯作為圖類的一個方法也可以,但是由于通過訪問頂點的pre參數已經可以回溯到上一個Vertex類,中間不涉及到圖的操作,因此可以安全地把它寫成一個靜態方法的形式,即不放在Graph類中:
1 #回溯路徑函數,傳入參數終點destination,返回路徑列表list(Vertex)
2 defreverse_trace(destination):3 current =destination4 trace =[destination]5 whilecurrent.get_pre():6 current =current.get_pre()7 trace.append(current)8 trace.reverse()9 return trace
這個函數返回的是一個路徑中順序出現的頂點Vertex類列表。
作為圖的一個練習和測試,我拿了北京市的地鐵信息。首先我在某一個文件夾里新建了編號為line1到line10的10個.txt文件,錄入了10條地鐵線的信息。后來又新增了13號線和15號線的信息。這些txt里的信息是這樣儲存的:a)一行一個站名,表示起點站;b)一行一個站名+一個數字,表示一個站和前一站的距離;c)一行兩個站名+一個數字,表示兩個站之間的距離。然后可以寫一個讀入的函數,把這些信息讀進來,每個站名就是一個id,形成一張圖,選定一個起點S以后執行Dijkstra算法,然后選定一個終點D回溯,就得到了從S到D的最短路徑。
如果只是想得到站與站之間的最短距離,這個方法當然是合適的;不過,如果考慮到換乘因素,就會發現這個算法沒有考慮到換乘的時間代價,這會使得搜索出一些需要換乘很多次的結果,而這在現實生活中并不是最快的。一個改進成本很小的方案是,把每個站所屬的線路號碼加在站名后面作為id的一部分,比如用“王府井1”作為一個id。同時,利用存儲形式c來規定換乘站的間距,比如規定“海淀黃莊4”和“海淀黃莊10”的距離。這樣一來就可以考慮換乘代價了。在這個思路下,Graph類相應的讀入函數:
1 classGraph:2 #讀取頂點文件(適用于地鐵路線例子的方法),傳入文件路徑path和線路編號subway
3 defread_in(self, path, subway):4 with open(path, 'r') as file:5 #按行讀取文件
6 line =file.readline()7 station =None8 whileline:9 info_list =line.split()10 #當行中僅含有1個元素時,僅創建
11 if len(info_list) == 1:12 station =info_list[0]13 station = station +str(subway)14 self.add_vertex(station)15 #當行中含有2個元素時,第1個元素為車站名稱,第2個元素為距上個車站距離
16 elif len(info_list) == 2:17 pre_station =station18 [station, distance] =info_list19 distance =int(distance)20 station = station +str(subway)21 self.add_vertex(station)22 self.add_edge(pre_station, station, distance)23 #否則當行中含有3個元素,前2個元素為車站,第3個元素為前二者間距
24 else:25 [station1, station2, distance] =info_list26 distance =int(distance)27 self.add_edge(station1, station2, distance)28 line = file.readline()
最后是初始化函數和個人偏愛的交互式菜單代碼,僅供參考:
1 #初始化函數(適用于地鐵路線例子的方法)
2 #讀入在file_path所示文件夾下的北京地鐵線數據,返回生成的圖
3 definitialize():4 graph =Graph()5 file_path = 'D:\Personal Documents\Project\BeijingSubway\line'
6 for i in range(1, 11):7 path = file_path + str(i) + '.txt'
8 graph.read_in(path, i)9 path = file_path + '13.txt'
10 graph.read_in(path, 13)11 path = file_path + '15.txt'
12 graph.read_in(path, 15)13 returngraph14
15
16 #交互式菜單函數(適用于地鐵路線例子的方法),傳入圖
17 defroute_find_menu(graph):18 print('>> 尋找乘地鐵最短路線,輸入0退出')19 start = input('>> 輸入起始站名+地鐵線(如“王府井1”):')20 while start != '0':21 graph.breadth_first_search(graph.get_vertex(start))22 destination = input('>> 輸入終點站名+地鐵線(如“王府井1”):')23 if destination == '0':24 break
25 trace_list =reverse_trace(graph.get_vertex(destination))26 print([x.get_id() for x intrace_list])27 start = input('>> 輸入起始站名:')
最后只要分別調用?graph1 = initialize()?創建實例,并且用?route_find_menu(graph1)?進入菜單即可。
總結
以上是生活随笔為你收集整理的python无向加权图_图:无向图(Graph)基本方法及Dijkstra算法的实现 [Python]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 插入数据到hive_Hive实现网站PV
- 下一篇: 车牌识别python实现ubuntu_p