上一篇文章,我们了解了广度优先搜索算法(BFS),BFS主要用来解决图的可达路径验证和最小路径问题,即从一个顶点A到另一个顶点B,是否有可达路径,如果有那么求出其到达的最少步骤。那么这里的最短路径就如果加上时间或者其他元素来表示的话,真的就是最短的吗?
这里,我们不妨将达到各个顶点的边加上一个花费时间来修饰,这里用来修饰边的元素,我们称之为权重,加上权重的边即为加权边,如下图所示:
还是上一篇文章的图,但是我们做了一些改动,这里我们使用有向图并且给各个边加上了权重,这里的权重代表的是每条边需要花费的时间,这样一来,我们上一篇文章中求出的 “最短路径” 还合适吗?
一目了然,加上权重之后,我们之前的最短路径已经不是真的 “最短”,那么我们需要引入一个专门的算法来处理这种情况——狄克斯特拉算法
算法简介
狄克斯特拉算法 是用来处理加权图的最短路径问题的一种图遍历算法,其具体思路如下:
- 找出 “最便宜” 的节点,即可在最小权重下到达的节点;
- 更新该节点的邻居的开销;
- 重复这个过程,直到对图中所有的节点都这样做了。
- 计算最终的路径
我们来看使用这种思路,解决上图的最短路径问题,分步拆解如下:
第一步:
从起点 Start出发,找出它能到达的最便宜的节点,从图中看出,节点Start相邻的节点有A 和 B,到达A节点需要5分钟,到达B节点需要 2分钟,至于其他节点对于Start节点来说目前并不清楚,我们假设为无穷大,我们用一个节点开销表来表示,如下所示:
节点 | 耗时 |
---|---|
A | 5 |
B | 2 |
C | ∞ \infty ∞ |
D | ∞ \infty ∞ |
E | ∞ \infty ∞ |
F | ∞ \infty ∞ |
End | ∞ \infty ∞ |
第二步:
由上表我们知道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);
}