图的最短路径
图最短路径的实现一般用 狄杰斯特拉算法。它能在一个加权图中计算从 A A A 点运动到 B B B 点的 最短路径。加权图如下所示:
传统的狄杰斯特拉算法并不支持负权值,这里把狄杰斯特拉算法做了一点点改进,还是称之为 狄克斯特拉算法。
狄克斯特拉算法原理
第一步:建立起点( A A A)到各个点的空表
起始 | 节点 | 权重代价 | 最优路径 | 是否访问 |
---|---|---|---|---|
A | C | ∞ \infty ∞ | - | 不可达 |
- | D | ∞ \infty ∞ | - | 不可达 |
- | E | ∞ \infty ∞ | - | 不可达 |
- | F | ∞ \infty ∞ | - | 不可达 |
- | G | ∞ \infty ∞ | - | 不可达 |
- | B | ∞ \infty ∞ | - | 不可达 |
其中 是否访问 表示是从起点 A A A 开始是否访问过该节点(例如 D D D)的每一个邻居节点。其取值状态如下:
- 正在遍历: 表示计算了从 A A A 到目标节点的路径,但是还没有计算从起始节点 A A A 到目标节点的邻居节点的路径;
- 不可达: 表示起始点 A A A 没有路径到达目标节点;
- 已遍历: 表示计算了从起始节点 A A A 到目标节点、目标节点的所有邻居节点的路径;
第二步:从起点初始化表格
起点
A
A
A 的邻居位
[
D
,
C
]
[D, C]
[D,C],则有:
- P a t h < A , D > = A → D Path_{<A, D>} = A \rightarrow D Path<A,D>=A→D, L o s s < A , D > = 10 Loss_{<A, D>} = 10 Loss<A,D>=10,设置为未遍历;
- P a t h < A , C > = A → C Path_{<A, C>} = A \rightarrow C Path<A,C>=A→C, L o s s < A , C > = 4 Loss_{<A, C>} = 4 Loss<A,C>=4,设置为未遍历;
更新图表数据如下:
起始 | 节点 | 权重代价 | 最优路径 | 是否访问 |
---|---|---|---|---|
A | D | 10 10 10 | A → D A \rightarrow D A→D | 正在遍历 |
- | C | 4 4 4 | A → C A \rightarrow C A→C | 正在遍历 |
- | E | ∞ \infty ∞ | - | 不可达 |
- | F | ∞ \infty ∞ | - | 不可达 |
- | G | ∞ \infty ∞ | - | 不可达 |
- | B | ∞ \infty ∞ | - | 不可达 |
第三步:取出未遍历节点中
例如:提取状态为 正在遍历 节点 (
D
D
D),节点
D
D
D 的邻居为
[
E
,
G
]
[E,G]
[E,G]。
- 计算 P a t h < A , E > = A → D → E Path_{<A, E>} = A \rightarrow D \rightarrow E Path<A,E>=A→D→E, L o s s < A , E > = 15 Loss_{<A, E>} = 15 Loss<A,E>=15;
- 如果 L o s s < A , E > Loss_{<A, E>} Loss<A,E> 小于目前路径权重代价,则更新当前路径,并设置节点 E E E 正在遍历;
- 计算 P a t h < A , G > = A → D → G Path_{<A, G>} = A \rightarrow D \rightarrow G Path<A,G>=A→D→G, L o s s < A , G > = 18 Loss_{<A, G>} = 18 Loss<A,G>=18;
- 如果 L o s s < A , G > Loss_{<A, G>} Loss<A,G> 小于目前路径权重代价,则更新当前路径,并设置节点 G G G 正在遍历;
- 设置节点 D D D 为 已遍历;
起始 | 节点 | 权重代价 | 最优路径 | 是否访问 |
---|---|---|---|---|
A | D | 10 10 10 | A → D A \rightarrow D A→D | 已遍历 |
- | C | 4 4 4 | A → C A \rightarrow C A→C | 正在遍历 |
- | E | 15 15 15 | A → D → E A \rightarrow D \rightarrow E A→D→E | 正在遍历 |
- | F | ∞ \infty ∞ | - | 不可达 |
- | G | 18 18 18 | A → D → G A \rightarrow D \rightarrow G A→D→G | 正在遍历 |
- | B | ∞ \infty ∞ | - | 不可达 |
第四步: 重复第三步直至遍历完所有节点
最终的结果如下:
起始 | 节点 | 权重代价 | 最优路径 | 是否访问 |
---|---|---|---|---|
A | D | 10 10 10 | A → D A \rightarrow D A→D | 已遍历 |
- | C | 4 4 4 | A → C A \rightarrow C A→C | 已遍历 |
- | E | 15 15 15 | A → D → E A \rightarrow D \rightarrow E A→D→E | 已遍历 |
- | F | 20 20 20 | A → D → E → F A \rightarrow D \rightarrow E \rightarrow F A→D→E→F | 已遍历 |
- | G | 18 18 18 | A → D → G A \rightarrow D \rightarrow G A→D→G | 已遍历 |
- | B | 24 24 24 | A → D → E → F → B A \rightarrow D \rightarrow E \rightarrow F \rightarrow B A→D→E→F→B | 终点 |
总结: 以上就得出了从起始点 A 依次到节点 D、C、E、F、G、B 的最短路径。
以上就是狄克斯特拉算法原理。
交换商品
假设你有一个乐谱,想和朋友交换钢琴,一群朋友之间的商品存在一个交换图(如下图所示),权重是需要额外支付的费用。那么,你的交换路径是怎么样的,才能使费用最少呢?注意其中存在一条负权边。
我们按照 狄克斯特拉算法原理 求解结果如下:
起始 | 节点 | 权重代价 | 最优路径 | 是否访问 |
---|---|---|---|---|
A | B | 5 5 5 | A → B A \rightarrow B A→B | 已遍历 |
- | C | − 2 -2 −2 | A → B → C A \rightarrow B \rightarrow C A→B→C | 已遍历 |
- | D | 20 20 20 | A → B → D A \rightarrow B \rightarrow D A→B→D | 已遍历 |
- | E | 25 25 25 | A → B → E A \rightarrow B \rightarrow E A→B→E | 已遍历 |
- | F | 35 35 35 | A → B → E → F A \rightarrow B \rightarrow E \rightarrow F A→B→E→F | 终点 |
最终的结果如下:
代码实现
建立网络
# 建立网络
Graph = {}
Graph['A'] = {"B":5, "C":0}
Graph['B'] = {"D":15, "E":20, "C":-7}
Graph['C'] = {"D":30, "E":35}
Graph['D'] = {"F":20}
Graph['E'] = {"F":10}
Graph['F'] = {}
建立表格
Infty = 100000
# 建立图表
# flag == -1:表示不可达
# flag == 0:表示未遍历
# flag == 1:表示已遍历
Table = {}
Table['A'] = {'Loss': Infty, 'Path':[], 'flag':-1} # 起点到A的路径信息
Table['B'] = {'Loss': Infty, 'Path':[], 'flag':-1} # 起点到B的路径信息
Table['C'] = {'Loss': Infty, 'Path':[], 'flag':-1} # 起点到C的路径信息
Table['D'] = {'Loss': Infty, 'Path':[], 'flag':-1} # 起点到D的路径信息
Table['E'] = {'Loss': Infty, 'Path':[], 'flag':-1} # 起点到E的路径信息
Table['F'] = {'Loss': Infty, 'Path':[], 'flag':-1} # 起点到F的路径信息
# 未遍历节点列表
NoTraverseNodes = set()
初始化表格
# 初始化图表
def InitialTable(table, start):
Table.pop(start) # 删除起始节点
global NoTraverseNodes
for node in Graph[start]:
NoTraverseNodes.add(node) # 添加进未遍历节点
Table[node]['Loss'] = Graph[start][node]
Table[node]['Path'] = [start, node]
Table[node]['flag'] = 0
return table
# 打印Table
for node in InitialTable(Table, 'A'):
print(node, ":", Table[node])
"""
B : {'Loss': 5, 'Path': ['A', 'B'], 'flag': 0}
C : {'Loss': 0, 'Path': ['A', 'C'], 'flag': 0}
D : {'Loss': 100000, 'Path': [], 'flag': -1}
E : {'Loss': 100000, 'Path': [], 'flag': -1}
F : {'Loss': 100000, 'Path': [], 'flag': -1}
"""
搜索最短路径
# 进行最短路径搜索
def SearchShortestPath(table, start):
global NoTraverseNodes
while len(NoTraverseNodes): # 遍历未遍历节点列表
node1 = NoTraverseNodes.pop()
for node2 in Graph[node1]: # 访问节点node1的每一个邻居
# 计算start到node2的损失值
loss = Table[node1]['Loss'] + Graph[node1][node2]
if loss < Table[node2]['Loss']: # 更新start到node2的路径
Table[node2]['Loss'] = loss
Table[node2]['Path'] = Table[node1]['Path'] + [node2]
Table[node2]['flag'] = 0
NoTraverseNodes.add(node2)
table[node1]['flag'] = 1 # 表示此节点已遍历过
return table
print()
for node in SearchShortestPath(Table, 'A'):
print(node, ":", Table[node])
"""
B : {'Loss': 5, 'Path': ['A', 'B'], 'flag': 1}
C : {'Loss': -2, 'Path': ['A', 'B', 'C'], 'flag': 1}
D : {'Loss': 20, 'Path': ['A', 'B', 'D'], 'flag': 1}
E : {'Loss': 25, 'Path': ['A', 'B', 'E'], 'flag': 1}
F : {'Loss': 35, 'Path': ['A', 'B', 'E', 'F'], 'flag': 1}
"""
最终的结果显示了,从节点 A A A 到各个节点的最短路径。
传统的 狄克斯特拉算法 无法解决负权边的最短路径搜索问题,此时可以使用 贝尔曼-福德 算法。
参考资料
- 《算法图解》