1、狄克斯特拉算法 (Dijkstra's algorithm)
对于广度优先搜索找出的路径,只适用于图中边的权值都相同的情况,如果权值不相同,可能找出的未必是最短路径。如图:
对于不同权值的图,我们使用狄克斯特拉算法寻找最短路径,该算法包含以下四个步骤:
- 找出“最便宜”的节点, 即可在最短时间内到达的节点。
- 更新该节点的邻居的开销, 其含义将稍后介绍。
- 重复这个过程, 直到对图中的每个节点都这样做了。
- 计算最终路径。
2、术语
狄克斯特拉算法用于每条边都有关联数字的图, 这些数字称为权重(weight) 。带权重的图称为加权图 (weighted graph) , 不带权重的图称为非加权图 (unweighted graph) 。 要计算非加权图中的最短路径, 可使用广度优先搜索 。 要计算加权图中的最短路径, 可使用狄克斯特拉算法 。 图还可能有环 , 而环类似下图。
狄克斯特拉算法只适用于有向无环图 (directed acyclic graph, DAG) 。
3、算法举例
- 问题背景
Rama, 想拿一本乐谱换架钢琴。Alex说: “这是我最喜欢的乐队Destroyer的海报, 我愿意拿它换你的乐谱。 如果你再加5美元, 还可拿乐谱换我这张稀有的Rick Astley黑胶唱片。 ”Amy说: “哇, 我听说这张黑胶唱片里有首非常好听的歌曲, 我愿意拿我的吉他或架子鼓换这张海报或黑胶唱片。 ”Beethoven惊呼: “我一直想要吉他, 我愿意拿我的钢琴换Amy的吉他或架子鼓。 ” 下图为他们的交换意愿:
![](https://i-blog.csdnimg.cn/blog_migrate/87d88c38dbb8c966a32ae9769c2ba77c.png)
这个图中的节点是大家愿意拿出来交换的东西, 边的权重是交换时需要额外加多少钱。Rama需要确定采用哪种路径将乐谱换成钢琴时需要支付的额外费用最少。 为此, 可以使用狄克斯特拉算法! 别忘了, 狄克斯特拉算法包含四个步骤。 在这个示例中, 你将完成所有这些步骤, 因此你也将计算最终路径。
首先创建一个表格, 在其中列出每个节点的开销。 这里的开销指的是达到节点需要额外支付多少钱。
在执行狄克斯特拉算法的过程中, 你将不断更新这个表。 为计算最终路径, 还需在这个表中添加表示父节点的列。
第一步 : 找出最便宜的节点。 在这里, 换海报最便宜,不需要支付额外的费用。
第二步 : 计算前往该节点的各个邻居的开销。
表中增加了架子鼓和吉他的开销,这些开销是通过海报交换所需支付的额外费用,所以他们的父节点均为海报。
再次执行第一步 : 下一个最便宜的节点是黑胶唱片——需要额外支付5美元。
再次执行第二步 : 更新黑胶唱片的各个邻居的开销。
对照上表,我们发现吉他和架子鼓的开销都更新了,这意味着经“黑胶唱片”前往“架子鼓”和“吉他”的开销更低, 因此你将这些乐器的父节点改为黑胶唱片。
于是,你计算出了到达钢琴的开销——40,而它的父节点为吉他。
最后, 对最后一个节点——架子鼓, 做同样的处理。
如果用架子鼓换钢琴, Rama需要额外支付的费用更少。 因此, 采用最便宜的交换路径时, Rama需要额外支付35美元 。
最短路径的确定:找到钢琴的父节点(架子鼓),通过沿父节点回溯, 便得到了完整的交换路径。
注意:不能将狄克斯特拉算法用于包含负权边的图 。 在包含负权边的图中, 要找出最短路径, 可使用另一种算法——贝尔曼-福德算法 (Bellman-Ford algorithm) 。
4、算法实现
python代码实现:
graph={}
#储存起点的邻居和权值
graph["start"]={}
graph["start"]["a"]=6
graph["start"]["b"]=2
#添加其他节点和邻居
graph["a"]={}
graph["a"]["fin"]=1
graph["b"]={}
graph["b"]["a"]=3
graph["b"]["fin"]=5
graph["fin"]={}#终点没有任何邻居
#储存父节点
parents={}
parents["a"]="start"
parents["b"]="start"
parents["fin"]=None
#记录处理过的节点
processed=[]
#储存每个节点的开销
infinity=float("inf")
costs={}
costs["a"]=6
costs["b"]=2
costs["fin"]=infinity
def find_lower_cost_node(costs):
lowest_cost=float("inf")
lowest_cost_node=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
node=find_lower_cost_node(costs)#在未处理的节点中找出开销最小的节点
while node is not None:#这个while循环在所有节点都被处理过后结束
cost=costs[node]
neighbors=graph[node]
for n in neighbors.keys():#遍历当前节点的所有邻居
new_cost=cost+neighbors[n]
if costs[n]>new_cost:#如果经当前节点前往该邻居更近
costs[n]=new_cost#就更新该邻居的开销
parents[n]=node#同时将该邻居的父节点设置为当前节点
processed.append(node)#将当前节点标记为处理过
node=find_lower_cost_node(costs)#找出接下来要处理的节点, 并循环
结果输出:
5、小结
- 广度优先搜索用于在非加权图中查找最短路径。
- 狄克斯特拉算法用于在加权图中查找最短路径。
- 仅当权重为正时狄克斯特拉算法才管用。
- 如果图中包含负权边, 请使用贝尔曼-福德算法。