本章内容:
- 继续图的讨论,介绍加权图——提高或降低某些边的权重
- 介绍迪克斯特拉算法,让你能够找出加权图中前往x的最短路径
- 介绍图中的环,迪克斯特拉算法只适用于有向无环图
在上一章中,我用广度优先算法可以找出段数最少的路径,但是如果要找出最快的路径,要用到另一种算法——迪克斯特拉算法(Dijkstra's algorithm)
目录
1 使用迪克斯特拉算法
关键理念:找出图中最便宜的节点,并确保没有到该节点的更便宜的路径!
迪克斯特拉算法包含4个步骤:
- 找出“最便宜”的节点,即可在最短时间内到达的节点
- 更新该节点的邻居的开销,检查是否有前往它们更短的路径
- 重复这个过程,直到对图中的每个节点都这样做了
- 计算最终路径
下面来看看如何对下图使用这种算法(每个数字都代表时间,单位分钟)
第一步:找出最便宜的节点。你站在起点,不知道是前往节点A还是B。
前往A需要6分钟,而前往B需哟啊2分钟,至于其他节点,你还不知道需要多久。由于你还不知道前往终点需要多长的时间,因此需要假设为无穷大。节点B是最近的——2分钟就可以到达。
节点 | 耗时 |
A | 6 |
B | 2 |
终点 | ∞ |
第二步:计算经节点B前往各个邻居的所需要的时间。
找到了一条通往A的更加近的路!经B到A的时间只需要5分钟!
节点 | 耗时 |
A | 5(=2+3) |
B | 2 |
终点 | 7(=2+5) |
对于节点B的邻居,如果找到前往它的更短的路径,就更新其开销。在这里你找到了:
-
前往节点A的更短路径(时间从6分钟缩短为5分钟
-
前往终点的路径更短(时间从无穷大缩短为7分钟)
第三步:重复!
重复第一步:找出可在最短时间内前往的节点,你对节点B执行了第二步,处节点B外,可在最短时间内前往的节点是节点A。
重复第二步:更新节点A的所有邻居的路径。
对每一个节点都需要运行迪克特斯拉算法,但是对终点无需这样做!
发现前往终点的时间只需要6分钟!
节点 | 耗时 |
A | 5 |
B | 2 |
终点 | 6 |
第四步:计算路径
先放着...
要是有一些一些疑惑,不妨看看这个
2 术语
- 迪克斯特拉算法用于每条边都关联数字的图,这些数字称为权重(weight)
- 带权重的图称为加权图(weighted graph),不带权重的图称为非加权图(unweighted graph)
- 无向图意味着两个节点彼此指向对方,其实就是环。在无向图中,每条边都是一个环
- 迪克斯特拉算法只适用于有向无环图(directed acyclic graph DAG)
- 如果有负权边,就不能使用迪克斯特拉算法,因为会导致算法不管用。需要用另外一种算法——贝尔曼-福德算法(Bellman-Ford algorithm)
3 算法实现
要编写这个问题的代码,需要三个散列表。
1、需要同时存储邻居和前往邻居的开销,还要使用另外一个散列表来表示边的权重(散列表里面包含散列表)
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'] = {}
#获取关系节点图
print(graph)
#获取某一节点的邻居
print(graph['start'].keys())
#获取从一个节点到邻居节点的权重
print(graph['start']['a'])
#输出
{'start': {'a': 6, 'b': 2}, 'a': {'fin': 1}, 'b': {'a': 3, 'fin': 5}, 'fin': {}}
dict_keys(['a', 'b'])
6
2、需要用一个散列表来存储每个节点的开销(即从出发到节点之间需要花多少时间)
当然这个散列表会更新的!
infinity = float('inf')
#这个用于记录python的无穷大
costs = {}
costs['a'] = 6
costs['b'] = 2
costs['fin'] = infinity
print(costs)
#输出
{'a': 6, 'b': 2, 'fin': inf}
3、需要一个散列表存储父节点
parents = {}
parents['a'] = 'start'
parents['b'] = 'start'
parents['fin'] = None
print(parents)
#输出
{'a': 'start', 'b': 'start', 'fin': None}
4、最后需要一个数组记录用过的节点,因为对于同一个节点,你不用多次修改
processed = []
最终的代码如下:
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'] = {}
infinity = float('inf')
costs = {}
costs['a'] = 6
costs['b'] = 2
costs['fin'] = infinity
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
#这个是初始化预设花销为最大,节点为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_lowest_cost_node(costs)
#在未处理的节点中找出开销最小的节点
while node is not None:
#这个while循环在所有的节点都被处理了之后结束
cost = costs[node]
#从起点到node节点所需要的花销
neighbors = graph[node]
#这个找到花销最小的节点node的邻居有哪些
for n in neighbors.keys():
#对这个节点的邻居进行遍历
new_costs = cost + neighbors[n]
#新的总花销就是从起点到node节点所需要的花销以及该节点到他的其中一个邻居的花销的和
if costs[n] > new_costs:
#如果起点到这个邻居的花销比新的总花销贵
costs[n] = new_costs
#那么就把这个起点到这个邻居的花销换成便宜的这个
parents[n] = node
#同时要把这个邻居的父节点换成node这个节点,因为经过node这个节点,起点到这个邻居的节点的花销会少一点
processed.append(node)
#同时记录一下这个被处理过的节点
node = find_lowest_cost_node(costs)
#然后再从剩下的花销节点里面找出花销最少的那个,重新一轮while循环
print(parents)
#输出
{'a': 'b', 'b': 'start', 'fin': 'a'}
5、对最后的结果进行处理优化
starting_point = 'start'
#起点
destination = 'fin'
#终点
line = [starting_point]
#我们很清楚的看到这个parents中键值对的关系:键代表父节点,值代表子节点
def find_son(father,diction):
#根据子节点找到父节点
for key_value in diction:
#这个里面返回的key_value是键
if diction[key_value] == father:
#如果某个键返回的值是这个要找的节点的孩子
line.append(key_value)
#那么就把这个父节点加入到这个数组里面去
return find_son(key_value,diction)
#进行递归,把这个找到的子节点作为父节点重新开始新一轮的找儿子哈哈哈哈
find_son(starting_point,parents)
spend = costs[destination]
#这个表示最少的花销
print('加权后的最短路径是:',line)
print('最少花销是:',spend)
输出:
加权后的最短路径是: ['start', 'b', 'a', 'fin']
最少花销是: 6
4 解决上一章的问题(猪猪去珊珊家的最短路径)
简单的举一反三一下
代码实现如下:
graph = {}
graph['浙江'] = {}
graph['浙江']['安徽'] = 200
graph['浙江']['江西'] = 400
graph['浙江']['福建'] = 350
graph['安徽'] = {}
graph['安徽']['湖北'] = 250
graph['安徽']['江西'] = 300
graph['江西'] = {}
graph['江西']['湖南'] = 350
graph['福建'] = {}
graph['福建']['江西'] = 250
graph['湖北'] = {}
graph['湖北']['湖南'] = 150
graph['湖南'] = {}
infinity = float('inf')
costs = {}
costs['安徽'] = 200
costs['江西'] = 400
costs['福建'] = 350
costs['湖北'] = infinity
costs['湖南'] = infinity
parents = {}
parents['安徽'] = '浙江'
parents['江西'] = '浙江'
parents['福建'] = '浙江'
parents['湖北'] = None
parents['湖南'] = None
processed = []
def find_lowest_cost_node(costs):
#这个找出所有节点的花销里面中最小的那一个
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
#返回所找的花销最小的那个节点
node = find_lowest_cost_node(costs)
#在未处理的节点中找出开销最小的节点
while node is not None:
#这个while循环在所有的节点都被处理了之后结束
cost = costs[node]
#从起点到node节点所需要的花销
neighbors = graph[node]
#这个找到花销最小的节点node的邻居有哪些
for n in neighbors.keys():
#对这个节点的邻居进行遍历
new_costs = cost + neighbors[n]
#新的总花销就是从起点到node节点所需要的花销以及该节点到他的其中一个邻居的花销的和
if costs[n] > new_costs:
#如果起点到这个邻居的花销比新的总花销贵
costs[n] = new_costs
#那么就把这个起点到这个邻居的花销换成便宜的这个
parents[n] = node
#同时要把这个邻居的父节点换成node这个节点,因为经过node这个节点,起点到这个邻居的节点的花销会少一点
processed.append(node)
#同时记录一下这个被处理过的节点
node = find_lowest_cost_node(costs)
#然后再从剩下的花销节点里面找出花销最少的那个,重新一轮while循环
print(parents)
starting_point = '浙江'
#起点
destination = '湖南'
#终点
line = [starting_point]
#我们很清楚的看到这个parents中键值对的关系:键代表父节点,值代表子节点
def find_son(father,diction):
#根据子节点找到父节点
for key_value in diction:
#这个里面返回的key_value是键
if diction[key_value] == father:
#如果某个键返回的值是这个要找的节点的孩子
line.append(key_value)
#那么就把这个父节点加入到这个数组里面去
return find_son(key_value,diction)
#进行递归,把这个找到的子节点作为父节点重新开始新一轮的找儿子哈哈哈哈
find_son(starting_point,parents)
spend = costs[destination]
#这个表示最少的花销
print('猪猪去珊珊家的最短的路线是:',line)
print('猪猪去珊珊家的最少路程是:',spend)
输出:
{'安徽': '浙江', '江西': '浙江', '福建': '浙江', '湖北': '安徽', '湖南': '湖北'}
猪猪去珊珊家的最短的路线是: ['浙江', '安徽', '湖北', '湖南']
猪猪去珊珊家的最少路程是: 600
爱你哦,等我来!
5 小结
- 广度优先搜索用于在非加权图中查找最短路径
- 迪克斯特拉算法用于在加权图中查找最短路径
- 仅当权重为正时迪克斯特拉算法才管用
- 如果图中包含负权边,去使用贝尔曼-福德算法