上一篇文章,我們瞭解了廣度優先搜索算法(BFS),BFS主要用來解決圖的可達路徑驗證和最小路徑問題,即從一個頂點A到另一個頂點B,是否有可達路徑,如果有那麼求出其到達的最少步驟。那麼這裏的最短路徑就如果加上時間或者其他元素來表示的話,真的就是最短的嗎?
這裏,我們不妨將達到各個頂點的邊加上一個花費時間來修飾,這裏用來修飾邊的元素,我們稱之爲權重,加上權重的邊即爲加權邊,如下圖所示:
還是上一篇文章的圖,但是我們做了一些改動,這裏我們使用有向圖並且給各個邊加上了權重,這裏的權重代表的是每條邊需要花費的時間,這樣一來,我們上一篇文章中求出的 “最短路徑” 還合適嗎?
一目瞭然,加上權重之後,我們之前的最短路徑已經不是真的 “最短”,那麼我們需要引入一個專門的算法來處理這種情況——狄克斯特拉算法
算法簡介
狄克斯特拉算法 是用來處理加權圖的最短路徑問題的一種圖遍歷算法,其具體思路如下:
- 找出 “最便宜” 的節點,即可在最小權重下到達的節點;
- 更新該節點的鄰居的開銷;
- 重複這個過程,直到對圖中所有的節點都這樣做了。
- 計算最終的路徑
我們來看使用這種思路,解決上圖的最短路徑問題,分步拆解如下:
第一步:
從起點 Start出發,找出它能到達的最便宜的節點,從圖中看出,節點Start相鄰的節點有A 和 B,到達A節點需要5分鐘,到達B節點需要 2分鐘,至於其他節點對於Start節點來說目前並不清楚,我們假設爲無窮大,我們用一個節點開銷表來表示,如下所示:
節點 | 耗時 |
---|---|
A | 5 |
B | 2 |
C | |
D | |
E | |
F | |
End |
第二步:
由上表我們知道Start節點到B節點的開銷最低爲2,那麼更新B節點的開銷爲2,我們用一個節點-開銷表來表示其中的關係,如下表所示:
父節點 | 節點 | 開銷 |
---|---|---|
Start | A | 5 |
Start | B | 2 |
接着我們從B節點出發找最便宜開銷節點,如下表所示:
節點 | 開銷 |
---|---|
D | 2 |
E | 7 |
那麼我們修改B的鄰居節點的開銷爲:
父節點 | 節點 | 開銷 |
---|---|---|
B | D | 5 |
B | E | 9 |
第三步:
接着重複上述步驟,直到得到如下表格:
父節點 | 節點 | 開銷 |
---|---|---|
Start | B | 2 |
B | D | 5 |
D | C | 6 |
C | End | 8 |
代碼實現
Python實現
def find_lowest_cost_node(costs,processed):
lowest_cost = float('inf') # 默認節點開銷爲無窮大
lowest_cost_node = None # 默認最小開銷節點爲None
for node in costs:
cost = costs[node] # 當前節點開銷
if cost < lowest_cost and node not in processed:
lowest_cost = cost
lowest_cost_node = node
return lowest_cost_node
def dijkstra(start,end,graph):
costs = graph[start] # 起點到鄰居節點的開銷映射
parents = {} # 保存最小開銷節點與父節點的關係映射
processed = [] # 已處理節點列表,防止節點重複處理
# 查找起點可以到達的最小開銷節點
node = find_lowest_cost_node(costs,processed)
while node is not None:
cost = costs[node] #
neighbors = graph[node] # 當前最小開銷節點的鄰居節點
for next_node in neighbors.keys():
# 從最小開銷節點到達其鄰居節點的合併開銷
new_cost = cost + neighbors[next_code]
if next_node not in costs or costs[next_code] > new_cost:
costs[next_code] = new_cost
parents[next_code] = node
processed.append(node)
node = find_lowest_cost_node(costs,processed)
path = [end] # 最短路徑節點列表
parent = parents[end]
while parent:
if parent == start:
path.append(parent)
break
path.append(parent)
if parent in parents.keys():
parent = parents[parent]
else:
break
path.append(start) # 加入起點
print('最短路徑爲 %s' % (' -> '.join(path[::-1])))
print('最小開銷爲: %d' % costs[end])
if __name__ == '__main__':
graph = dict()
graph['Start'] = dict()
graph['Start']['A'] = 5
graph['Start']['B'] = 2
graph['A'] = dict()
graph['A']['C'] = 3
graph['B'] = dict()
graph['B']['D'] = 3
graph['B']['E'] = 7
graph['C'] = dict()
graph['C']['End'] = 1
graph['D'] = dict()
graph['D']['C'] = 1
graph['E'] = dict()
graph['E']['F'] = 2
graph['F'] = dict()
graph['F']['End'] = 1
graph['End'] = {}
dijkstra('Start', 'End', graph)
輸出結果:
最短路徑爲: Start -> B -> D -> C -> End
最小開銷爲: 7
Java實現
//查找開銷最小的節點
private String findLowestCostNode(Map<String,Integer> costs,Set<String> processed) {
int lowestCost = Integer.MAX_VALUE;
String lowestCostNode = null;
for (String node : costs.keySet()) {
int cost = costs.get(node);
if (cost < lowest_cost && !processed.contains(node)) {
lowest_cost = cost;
lowest_cost_node = node;
}
}
return lowest_cost_node;
}
/**
* 使用狄克斯特拉算法查找加權邊最優路徑
* @param start 起點
* @param end 終點
* @param graph 圖結構散列表
*/
public void search(String start,String end,Map<String,Map<String,Integer>> graph) {
//起點的鄰居節點的開銷映射
Map<String, Integer> costs = graph.get(start);
//各個最小開銷節點的父節點
Map<String,String> parents = new HashMap<>();
//已處理節點的集合
Set<String> processed = new HashSet<>();
//查找起點可達的最小開銷節點
String node = findLowestCostNode(costs,processed);
while (node != null && graph.get(node) != null) {
int cost = costs.get(node);
Map<String,Integer> neighbors = graph.get(node);
for (String n : neighbors.keySet()) {
int new_cost = cost + neighbors.get(n);
if (!costs.containsKey(n) || costs.get(n) > new_cost) {
costs.put(n,new_cost);
parents.put(n,node);
}
}
processed.add(node);
node = findLowestCostNode(costs,processed);
}
print(start,end,parents,costs.get(end));
}
private void print(String start,String end,Map<String,String> parents,int cost) {
Stack<String> stack = new Stack<>();
String parent = parents.get(end);
while (parent != null) {
if (start.equalsIgnoreCase(parent)) {
stack.push(parent);
break;
}
stack.push(parent);
parent = parents.get(parent);
}
StringBuffer path = new StringBuffer();
while (!stack.empty()) {
String node = stack.pop();
if (path.length != 0) {
path.append(" -> ");
}
path.append(node);
}
System.out.println("最優路徑: " + start + " -> " + path.toString() + " -> " + end);
System.out.println("總開銷爲: " + cost);
}