《算法图解》学习笔记-第七章-狄克斯特拉算法
前言
本文为自己的算法学习笔记,用来将所学知识及时输出
所用书籍:《算法图解》
7.1 加权图
定义:设G为图,对图的每一条边e来说,都对应于一个实数W(e)(可以通俗的理解为边的“长度”,只是在数学定义中图的权可以为负数),我们把W(e)称为e的“权”。把这样的图G称为“加权图”——百度百科
个人理解:类似带宽,同样的物理长度,千兆带宽(加权图)比百兆带宽(图),传输的数据量多。
狄克斯特拉算法就是用来在加权图中查找最短路径的办法。
7.2 使用狄克斯特拉(Dijkstra)算法
在下面这幅图中数字代表着时间,单位为分钟,如何找出从起点到终点的耗时最短路径?
如果用上一章讲的广度优先搜索,你可能会得到:起点 ——> A ——> 终点 这样的路径,但它是耗时最短的路径吗?显然不是,他需要耗时7分钟,而我们仔细观察一下就会发现:由起点 ——> B ——> A ———> 终点 只需6分钟。
狄克斯特拉算法包含四步:
- 找出可在最短时间内到达的节点。
- 更新其邻居节点开销。
- 重复上述过程,直到对所有节点都进行过此操作。
- 计算出到终点的最优路径。
第一步:由起点出发,到达A点需要6分钟,到达B点需要2分钟,到达终点所需时间暂时不知,暂设为∞(无穷大)。具体表格如下:
节点 | 时长 | 父节点 |
A | 6 | 起点 |
B | 2 | 起点 |
终点 | ∞ | 未知 |
第二步:我们发现由起点到达B节点耗时最短,因此来到B节点。由B节点出发到达A节点需要3分钟,到达终点需要5分钟。现在我们了解了到达由起点到达终点所需的时长:2+5=7(分钟),并且发现了到达A节点的更短路径:起点 ——> B ——> A 只需用时5分钟,更新表格 :
节点 | 时长 | 父节点 |
A | ~~6~~ 5 | ~~起点~~ B |
B | 2 | 起点 |
终点 | 5 | B |
第三步:对B节点重复上述操作。
重复第一步:发现到A节点所用时间最少,来到A节点。
重复第二步:更新A节点的邻居节点开销,发现了到达终点的更短时间为6分钟,更新表格如下:
节点 | 时长 | 父节点 |
A | 5 | B |
B | 2 | 起点 |
终点 | 6 | A |
最后一步:由终点开始回溯,找出最短路径。查找终点的父节点发现其为A,而A节点的父节点为B,B节点的父节点为起点,至此我们发现了由起点到终点耗时最短的路径:起点 ——> B ——> A ——> 终点 耗时6分钟。
7.3 术语
- 权重:狄克斯特拉算法用于每条边带有关联数字的图,这些数字被称为权重(weight)。
- 加权图&非加权图:带有权重的图称为加权图(weighted graph),反之称为非加权图(unweighted graph)。
- 环:如下图一样,从一个节点出发走一圈又回到当前节点。
上一章讲过有向图和无向图,无向图的关系双向的,也可以看做是一个环,而若一直沿着环循环前进则永远达不到终点,所以狄克斯特拉算法只适用于有向无环图(directed acyclic)
7.4 负权边
假设你从A节点到B节点的时候遇到了时空乱流,唰一下你回到了30分钟前,因为时间倒退,这时你从A节点到B节点的时间应为负值,即权重为负值(当然这并不严谨,只是用来举个例子),如下图所示:
第一步:从start开始,查找用时最短路径,得到下表:
节点 | 时长 | 父节点 |
A | 5 | start |
B | 2 | start |
end | ∞ | unknown |
第二步:发现到B节点用时最短,来到B节点并更新其邻居节点开销,如下表:
节点 | 时长 | 父节点 |
A | 5 | start |
B | 2 | start |
end | 12 | B |
将B节点标记为已处理
第三步:重复以上操作,当来到A节点发现,由A节点到B节点所用时间远远小于从起点到A节点
因此准备更新B节点,这时要注意了,因为B节点已经被标记过,这意味着已经没有前往该节点的更短路径,可你明白这并不正确。end节点没有邻居,算法结束。
如下表:
节点 | 时长 | 父节点 |
A | 5 | start |
~~B~~ | ~~-25~~ | ~~A~~ |
end | 12 | B |
因此狄克斯特拉算法不能解决带负权的加权图。
7.5 狄克斯特拉算法的Python实现
代码如下:
'''
Dijkastra Algorithm 迪克斯特拉算法(仅当权值为正时使用)
'''
# Graph 权重关系表
graph = {}
graph['start'] = {} # 由起点到其邻近节点的权值的信息
graph['start']['a'] = 6
graph['start']['b'] = 2
graph['a'] = {} # 由a节点到其邻近节点的信息
graph['a']['fin'] = 1
graph['b'] = {} # 由b节点到其邻近节点的信息
graph['b']['a'] = 3
graph['b']['fin'] = 5
graph['fin'] = {}
# cost 开销表
infinity = float('inf') # 定义无穷大量
costs = {}
costs['a'] = 6
costs['b'] = 2
costs['fin'] = infinity # 由于不清楚start距fin有多远,暂设为无穷大
# parents 父节点表
parents = {}
parents['a'] = 'start'
parents['b'] = 'start'
parents['fin'] = None
# 记录已处理的节点
processed = []
# 定义查询最小消耗节点函数
def find_lowest_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
# 算法主要部分
def dijkastra():
node = find_lowest_cost_node(costs) # 找到最小开销节点
while node is not None: # 直到开销表所有节点都被处理过
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_lowest_cost_node(costs) # 获取下一个最小开销节点
# test
if __name__ == '__main__':
dijkastra()
print(processed) # ['b', 'a', 'fin']
print(parents) # {'a': 'b', 'b': 'start', 'fin': 'a'}
print(costs) # {'a': 5, 'b': 2, 'fin': 6}
end:总结
- 图的边均带有相应权值的图名为加权图
- 两个节点互相指向称为环,无向图可看做环。
- 狄克斯特拉算法可以解决查找加权图最优路径的问题。
- 狄克斯特拉算法只可用于解决有向无环图。
- 狄克斯特拉算法不能解决负权值图的最优路径问题。