学习笔记
学习书目:《算法图解》- Aditya Bhargava
文章目录
- 具体步骤实现
狄克斯特拉算法
在上一个Blog中,我们用广度优先搜索找到了从家到公园换乘最少的路线,即家–1-->路–>A–3路–>E–5路–>公园。
但是有的时候,我们要寻找的是从家到公园耗时最短的路线,这时广度优先搜索就不顶用了,我们将会使用狄克斯特拉算法解决这个问题。
我养了一直兔子,它叫小黄,我在家里给小黄从它的窝到餐厅搭一个兔子专用通道:
假设小黄从始至终保持匀速前进,且到达一个地点不做停留。
试问,从窝到餐厅,小黄走哪条路线耗费时间最短?在这种情境中,这个问题也可以理解为,小黄走哪条路线的总路程最短?
如果用广度优先搜索,小黄可能会选窝–>厕所–>餐厅或者窝–>客厅–>餐厅,但是这两条路线所的总长为7m,并不是最短路线。
下面我们用狄克斯特拉算法寻找最短路线。狄克斯特拉算法包含4个步骤:
(1) 找出“最便宜”的节点,即可在最短时间内到达的节点。
(2) 更新该节点的邻居的开销,其含义将稍后介绍。
(3) 重复这个过程,直到对图中的每个节点都这样做了。
(4) 计算最终路径。
具体步骤实现
- 第一步:找到最便宜的节点
现在,小黄蹲在自己的窝里,它前往客厅要2m,前往厕所要6m,至于其他节点,小黄现在不知道要多远,对于这种不知道路程长短的节点,小黄先设置其长度为无穷大
节点 | 长度 |
客厅 | 2 |
厕所 | 6 |
餐厅 |
我们看到客厅离小黄最近,前往客厅只需要2m。
- 第二步:计算由客厅前往其他邻居所需的路程
节点 | 长度 |
客厅 | 2 |
厕所(更新) | 5 |
餐厅(更新) | 7 |
小黄找到了一条前往厕所更短的路,只需要5m;同时,它也找到了前往餐厅更短的路,只需要7m。因为我们找到了前往厕所和餐厅更短的路,所以我们在表中更新其开销。
- 第三步:重复
重复第一步:找出除客厅节点以外的,可在最短路程内前往的节点。小黄观察到,除了客厅以外,可在最短路程内前往的节点是厕所。
重复第二步:更新厕所节点所有邻居的开销。
节点 | 长度 |
客厅 | 2 |
厕所 | 5 |
餐厅(更新) | 6 |
更新后,小黄发现,前往餐厅只需要6m了。
小黄对每个节点都运行了狄克斯特拉算法(不需要对终点这样做),现在它知道了:
- 前往客厅节点只需要2m
- 前往厕所节点只需要5m
- 前往餐厅节点只需要6m
现在,小黄知道,最短路径是从窝–>客厅–>厕所–>餐厅,总路程只要6m.
上一个Blog里,使用了广度优先搜索来查找两点之间的最短路径,那时“最短路径”的意思是段数最少。在狄克斯特拉算法中,小黄给每段都分配了一个数字或权重,因此狄克斯特拉算法找出的是总权重最小的路径。
术语
狄克斯特拉算法用于每条边都有关联数字的图,这些数字称为权重。带权重的图称为加权图,不带权重的图称为非加权图。
要计算非加权图中的最短路径,可使用广度优先搜索。要计算加权图中的最短路径,可使用狄克斯特拉算法。图还可能有环,这意味着我们可从一个节点出发,走一圈后又回到这个节点。
值得注意的是,无向图意味着两个节点彼此指向对方,其实就是环。在无向图中,每条边都是一个环。狄克斯特拉算法只适用于有向无环图。
跳蚤市场
假设我们小时候都参加过跳蚤市场,我们知道跳蚤市场中非常自由,大家可以以物换物,直接用钱购买物品,甚至是以钱+物换物的方式进行交易。
现在,我有本《数学之美》,我可以有以下交易选项:
上图说的是,如果我想得到《编程之美》可以用《数学之美》去换,且不用多花一分钱;而得到《西瓜书》则要用《数学之美》再加5元去换取;而如果我想得到钢笔就要用《西瓜书》再加15元去换…以此类推
现在我想用最少的代价得到移动硬盘,这个过程我将借助狄克斯特拉算法来完成,为了得到最终路径,我还要在我的表中添加父节点的列:
父节点 | 节点 | 开销 |
《数学之美》 | 《编程之美》 | 0 |
《数学之美》 | 《西瓜书》 | 5 |
– | 钢笔 | |
– | PS4手柄 | |
– | 移动硬盘 |
具体步骤实现
- 找出最便宜的节点
我们首先要找出图中最便宜的节点,并确保没有到该节点的更便宜的路径。可以看到,最便宜的节点是《编程之美》节点,我们不需要花一分钱,就可以换取到。
- 计算前往该节点的各个邻居的开销
父节点 | 节点 | 开销 |
《数学之美》 | 《编程之美》 | 0 |
《数学之美》 | 《西瓜书》 | 5 |
《编程之美》 | 钢笔(更新) | 30 |
《编程之美》 | PS4手柄(更新) | 35 |
– | 移动硬盘 |
- 再执行第一步
可以看到,除了《编程之美》节点以外,最便宜的节点是《西瓜书》节点。
- 再执行第二步
父节点 | 节点 | 开销 |
《数学之美》 | 《编程之美》(已用) | 0 |
《数学之美》 | 《西瓜书》 | 5 |
《西瓜书》 | 钢笔(更新) | 20 |
《西瓜书》 | PS4手柄(更新) | 25 |
– | 移动硬盘 |
- 循环往复
更新后,我们发现下一个最便宜的是钢笔,因此更新其邻居的开销:
父节点 | 节点 | 开销 |
《数学之美》 | 《编程之美》(已用) | 0 |
《数学之美》 | 《西瓜书》(已用) | 5 |
《西瓜书》 | 钢笔 | 20 |
《西瓜书》 | PS4手柄 | 25 |
钢笔 | 移动硬盘(更新) | 40 |
最后,除了终点,只剩下PS4手柄了,我们更新其邻居的开销:
父节点 | 节点 | 开销 |
《数学之美》 | 《编程之美》(已用) | 0 |
《数学之美》 | 《西瓜书》(已用) | 5 |
《西瓜书》 | 钢笔(已用) | 20 |
《西瓜书》 | PS4手柄 | 25 |
PS4手柄 | 移动硬盘(更新) | 35 |
- 结果
通过狄克斯特拉算法,我只需要花35元加一本《数学之美》就可以换到移动硬盘啦!我的换取过程是:《数学之美》–>《西瓜书》–>PS4手柄–>移动硬盘
负权边
如果发生了点意外,有个同学A现在立马要用《西瓜书》,但它只有《编程之美》,所以他愿意用7元加《编程之美》换取西瓜书:
那么现在我有两种选择可以得到《编程之美》:
(1)不花一分钱换《编程之美》
(2)先花5元买《西瓜书》,再和A同学交易《编程之美》,那么我们将得到《编程之美》加2元
显然方法2更好一些。
虽然我们可以这样做,但是狄克斯特拉算法并不支持这样的方式,如果有负权边,就不能使用狄克斯特拉算法。因为负权边会导致这种算法不管用。
这是因为狄克斯特拉算法这样假设:对于处理过的《编程之美》节点,没有前往该节点的更短路径。这种假设仅在没有负权边时才成立。因此,不能将狄克斯特拉算法用于包含负权边的图。在包含负权边的图中,要找出最短路径,可使用另一种算法—贝尔曼-福德算法.
看不懂上面这句话没关系,我们按照狄克斯特拉算法的流程画两个表格:
父节点 | 节点 | 开销 |
《数学之美》 | 《编程之美》 | 0 |
《数学之美》 | 《西瓜书》 | 5 |
– | PS4手柄 |
找到最便宜节点《编程之美》,更新其邻居的开销:
父节点 | 节点 | 开销 |
《数学之美》 | 《编程之美》 | 0 |
《数学之美》 | 《西瓜书》 | 5 |
《编程之美》 | PS4手柄(更新) | 35 |
找到最便宜节点《西瓜书》,更新其邻居的开销:
父节点 | 节点 | 开销 |
《数学之美》 | 《编程之美》(已用)(更新) | -2 |
《数学之美》 | 《西瓜书》 | 5 |
《编程之美》 | PS4手柄 | 35 |
我们看到《编程之美》已经被我们处理过了,这里却对其进行了更新。在狄克斯特拉算法中这非常危险,因为节点一旦被处理,就意味着没有前往该节点的更便宜的路径,但是我们却找到了更便宜的路径。
python实现
我们以下面这幅有向图为例,用python实现狄克斯特拉算法:
要编写解决这个问题的代码,需要三个散列表:
随着算法的进行,我们还要不断的更新散列表costs和parents.
现在我们来写python代码。
graph散列表:
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'] = {}
costs散列表:
#定义无穷大
infinity = float('inf')
costs = {}
costs['a'] = 6
costs['b'] = 2
costs['fin'] = infinity
parents散列表:
parents = {}
parents['a'] = 'start'
parents['b'] = 'start'
parents['fin'] = None
python代码:
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
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:
parents[n] = node
processed.append(node)
node = find_lowest_cost_node(costs)
print(parents)