grokking algorithms--第七章-Dijkstra's algorithm迪杰斯特拉算法中文翻译

第七章 迪杰斯特拉(Dijkstra’s algorithm)算法
在本章中:
我们继续讨论图,你会学到有权重的图,每条边上有不同的权重值。
你将学到迪杰斯特拉算法,这个算法可以告诉我们在有权值的图中“到终点X的最近的路 时是哪一条?”
你会学到图中的闭环,这种情况下迪杰斯特拉算法是不起作用的。
这里写图片描述
在上一章中,我们找到了一条从A点到B点最短的路,但是,它却不一定是最快的路,它是 最短的路,因为它经过的路段最少,但是如果你将每段路需要花费的时间加上,那么你可以 找到更快一点的路。如下图黑色标出的部分。
这里写图片描述
上一章中用了广度优先搜索算法,该算法可以找到经过节点最少的路径。若果你想找到用时
最短的路径呢?你可以换一种不同的算法—迪杰斯特拉算法。
我们来看下它是怎么发挥超能力的?
这里写图片描述

该图中,到达每一个节点都需要一定的时间,你可以利用迪杰斯特拉算法(Dijkstra’s algorithm)来找到从起点到终点花费时间最短的路径。 如果你利用广度优先算法,你可以找到的最短路径是起点—>A—>终点。
这里写图片描述
但是这条路线需要7分钟,让我们来找一条花费时间更短的路径,这需要四步: 1.找到时间花费最少的节点。
2.更新这个节点的邻居节点的花费时间。 3.重复以上步骤,知道图中所有的节点都遍历过了。 4.计算最终路径所需的时间。
步骤1:找到花费时间最短的节点。 从起点开始,如果你要去A点或B点,那么它们分别需要耗费多少时间呢?
这里写图片描述

从起点到A节点需要6分钟,从起点到B点需要2分钟,剩下的节点需要多长时间,我们暂时 不知道。
这里写图片描述
因为我们不转掉到达终点需要花费多长时间, 我们用无穷大来标示。 现在节点B是花费时间最短的节点,它只需要2分钟。
步骤2:计算通过节点B,到达它的邻居节点需要花费的时间。
这里写图片描述
这里写图片描述
但是若是从起点经过B点再到达A点,只需要花费5分钟。
这里写图片描述
对于B的邻居节点,当你发现有花费时间更短的路径时,更新该邻居节点的花费时间。在这 个例子中,你发现

• 到达A节点用时更短的路径(从6分钟减少到5分钟)
• 到达终点更短的路径(时间从无穷大到7分钟)
步骤3 重复上面的步骤1和步骤2. 重复步骤1:找到用时最短的节点。节点B已经计算过了,用时第2少的是节点A。
这里写图片描述
重复步骤2:更新A的邻居节点的耗费时间。
这里写图片描述
WOO,现在到达终点只需要花费6分钟。 你已经对每一个极点都执行过迪杰斯特拉算法了,这个例子中,我们知道: • 到达B节点,需要花费2分钟;
• 到达A节点,需要花费5分钟;
• 到达终点,需要花费6分钟。
这里写图片描述
下一步中,我们学习如何计算最终路径,现在我们直接给出最终路径: 起点—>B—>A—终点。
这里写图片描述

广度优先深度算法没有找出用时最短的路径,因为用时最短的路径它有3个边。但是存在有2 个边也可以到达终点的路径。因此广度优先算法找到的路径是只有2个边。
这里写图片描述

上一章中吧,我们用广度优先算法找到了两点之间的最少路径。换句话说,最少路径是边最
少,但是若是图中的每条边都有权值,迪杰斯特拉算法可以找到花费最短时间的路径。
这里写图片描述

复习一下,第杰斯特拉算法算法总共分四步: 1.找到有最小权值的节点,这个节点花费的时间最少; 2,若是经过它到达它的邻居节点,花费的时间更短,更新它的邻居节点的花费时间(权 值 );
3.一直重复知道所有的节点都遍历过了; 4.计算最终路径(下一部分会讲到)
术语
现在我们来看几个例子,在这之前,我们先来阐明几个术语。
当我们用迪杰斯特拉算法时,图中的每个边的树枝,我们称之为权重。
这里写图片描述

图中边上有数字的图成为加权图,若是图中不带有加权值的,成为非加权图。
这里写图片描述

当计算非加权图的最短路径时,用广度优先搜索算法,当计算加权图的最短路径时,用迪杰
这里写图片描述
也就是说,你可以从起点开始,绕一圈,然后回到起点。假设在一个有闭环的图中,你想找
到最短的路径。这里写图片描述
你会选择带有闭环的路径吗?是的,你可以选择避开闭环的路径,它花费5(分钟)
这里写图片描述

或者你选择经过闭环的路径,整个路径的总花费时间是13(分钟)
这里写图片描述
或者若是你愿意经过闭环2次,它的总耗时是21(分钟)
这里写图片描述
也就是说,你每一次通过闭环,它花费你8(分钟),若是选择的路径中带有闭环,那么它 一定不是最短的路径。
最后,还记得我们第6章中讨论的有向图和无向图吗?
这里写图片描述
无向图意味着两个节点指向彼此,也就是说,无向图和闭环是一个意思。
这里写图片描述
无向图中,每条边都增加一个闭环。迪杰斯特拉算法只适用于无回路有向图,简称DAGs
交易(置换)钢琴

术语介绍完了,我们来看另一个例子。这是Rama这里写图片描述, 他是想用一本乐谱书换取一架真正的钢琴。
Alex想用海报来交换Rama的乐谱书,“这是我最喜欢的Destroyer乐对的海报,或者用Rick Astley的珍贵的黑胶唱片和你交换,但是你要付我5美元”
Amy说:“我听说这个唱片中有首歌很好听,我想用我的吉他后者架子鼓来换取海报或唱片”。
Beethoven说:“我想要吉他,我想用我的钢琴来换Amy的吉他或者架子鼓”。
完美!!!加上一点点钱,Rama就可以用一本乐谱书来换取一架真正的钢琴了。现在,他 需要计算怎样才能花最少的钱来换回钢琴。
这里写图片描述
在这张图中,节点是Rama可以换回的项(海报、唱片、架子鼓、吉他、钢琴等),边上的 权值是每一项需要花费的美元数目。所以,他可以支付30美元用海报来换吉他,或用黑胶唱 片来换吉他,这样需要支付15美元。Rama需要将乐谱书换成真正的钢琴,怎样才能花钱最 少呢?迪杰斯特拉算法可以解救他。记住,迪杰斯特拉算法有四步,在这个例子中,做完这 四步后,就会找到花钱最少的置换方法了。
在你开始之前,你需要一些准备。建一张表格,将每个节点和该节点的花费美元数存储进去,
这里写图片描述
你会一直更新这张表格,需要计算最优路径,你需要将它们的父节点也放到一张表格中去。
这里写图片描述
我们来它是演示如何工作的,伙伴们,让我们开动吧!
步骤1:找到最便宜的节点(花费的美元数目最少) 这个列子中,海报是最便宜的,不需要花钱。还有更不便宜的方法去换取海报吗?这时一个 很重要的点。你可以帮Rama找到其他方式。让他花少于0美元来得到海报吗?答案是:没有。 没有比这更省钱的方案了。
有另一种思路来理解这个假设,你需要从家去公司。
这里写图片描述

如果你穿过学校,需要花费2分钟,如果你走经过公园的那条路,它会少于2分钟吗?不会, 这是不可能的,因为要花费更长的时间,需要6分钟吧到达公园。反过来说,你能找到去公 园更近的路吗?当然!
这里写图片描述
这是迪杰斯特拉算法的精髓。找到图中最便宜的路径,它一定要是各种路径中最便宜的。 回到原来的例子中,海报就是最便宜的。
步骤2:找到经过该节点到达它的邻居节点的花费。
这里写图片描述
我们把得到吉他或者架子鼓的路径和花费也记录到了表格中。因此,海报是吉他或者架子鼓
的父节点。也就是说,为了得到吉他或者架子鼓,你要先得到海报,然后再用海报来换取架
子鼓。
这里写图片描述
重复步骤1:找到第二便宜的节点,黑胶唱片,得到它,需要花费5美元。 重复步骤2:更新它的邻居节点的值。
这里写图片描述

现在,架子鼓和吉他的花费的金额都被更新了。也就是说,通过黑胶唱片来换取架子鼓或者
吉他更便宜。因此,现在把黑胶唱片来作为架子鼓和吉他的父节点。
现在,低音吉他是最便宜的项了,更新它的邻居节点的花费的金额数目。
这里写图片描述
现在,你有钢琴的价格了,通过吉他来换钢琴,吉他是钢琴的父节点。
现在,更新最后一个节点—架子鼓。
这里写图片描述
Rama可以通过架子鼓来换取钢琴,这样更便宜,只需要35美元!
现在正如我所承诺的,我们获取了开销最少的换取方式,你可以指出来路径吗?首先,找到
钢琴的父节点—架子鼓。

这里写图片描述
也就是说,Rama需要通过架子鼓来换取钢琴。
让我们来看下路径。
架子鼓是钢琴的父节点。
这里写图片描述
黑胶唱片是架子鼓的父节点。
这里写图片描述
所以,Rama可以通过黑胶产品获取架子鼓,当然,他通过乐谱书获取黑胶唱片,通过回溯 哥哥节点的父节点,我们可以得到整个路径。
这里写图片描述
乐谱书—>黑胶唱片 黑胶唱片—>架子鼓

架子鼓—>钢琴
这里写图片描述
到目前为止,我们逐步用到了最短路径这个词:计算两个地点或两个人之间的最短距离。我 希望让大家意识到最短路径并不是指地理位置上的最短路径。它可以是各种事物的最小值。 在这个例子中最短路径是Rama花费最少的钱来用乐谱书换取一架真正的钢琴。多亏了迪杰 斯特拉算法。
带有权重的边
在这个置换的例子中,Alex提供了两种物品来交换乐谱书。 这里写图片描述
假设Sarah用唱片交换海报,但是Sarah给Rama 7美元,Rama不需要给Sarah钱,反而他可以 获得7美元,这在图中怎么表示呢?
这里写图片描述

产品到海报的边的权重是负的。Rama如果和Sarah交换,Rama可以得到7美元,现在Rama有 两种方式得到海报。
第1种方式:拿乐谱书直接和海报交换,他得到0美元;
第2种方式:先拿乐谱书和唱片交换,再用唱片和海报交换,这样他可以得到2美元。
这里写图片描述
第2种交换很有意思。Rama可以得到2美元。你还记得吗?Rama可以用海报换取架子鼓,现
在有两种路径可以选择。
第2种路径他可以少花2美元。他会选这条路径,对吧?
但是若是你用迪杰斯特拉算法,Rama就会选错路径。当图中存在这种带有父权值的边时, 迪杰斯特拉算法是不适用的,我们具体来看下。首先,将花费的美元书填入表格中。
这里写图片描述

现在找到最便宜的节点。
然后更新它的邻居节点的花费的美元数
在这个例子中,海报是最便宜的节点。因此,根据迪杰斯特拉算法,没有比通过乐谱书里换 取海报这条路径更便宜的了。这花费是0美元。(但是你知道这是错的),不管怎么说,让 我们更新它的邻居节点的值。
这里写图片描述
OK,架子鼓需要花费35美元。
现在,我们找到第二便宜的节点,即黑胶唱片。
这里写图片描述
更新它的邻居节点的花费。
这里写图片描述

但是这个海报节点已经处理过了,你要更新海报节点的值的话,这是一个值得关注的点,当
你处理完一个节点时,意味着美元其他更便宜的路径到达它,但是现在你发现了更便宜的路
径,架子鼓没有邻居,因此算法运行到此结束。表中是花费的美元数。
这里写图片描述
获取架子鼓需要花费35美元,但是你知道有一种路径,只需要花费33美元,到那时迪杰斯特 拉算法美元发现它。因为根据迪杰斯特拉算法,你已经遍历过海报(节点),那么就没有更 便宜的路径到达这个节点了,这个假设成立的前提是书中的边的权值不包含负数。因此每当 图中带有父权值的边时,迪杰斯特拉算法不适用。如果你要计算类似图,有另一种算法,贝 尔曼·福特算法(Bellman-Ford 算法),这个算法不在本书的讲解范围,你可以通过上网来学 习它。
代码执行
让我们看下这个算法用python代码如何执行。下面是我需要用到的例子。
这里写图片描述
需要用到3张Hash表
这里写图片描述

当算法执行时,你需要更新costs表和parents表,首先,你需要添加图,先创建一个空图。 上一章中,你学习了如何在图中,记录一个节点的邻居节点。
graph = {}
现在,我们不只需要记录邻居节点,还需要记录权值(costs),比如这两个节点,A和B。
怎么记录每条边的权值呢?可以试试这种Hash表。
graph[“you”] = [“alice”, “bob”, “claire”]
所以这是个Hash表,你可以得到节点start的所有邻居节点a和b
这里写图片描述
从start节点到A点有一条边,从start到B也有一条边,若是想得到每条边的权值,怎么办呢? graph[“start”] = {}
graph[“start”][“a”] = 6
graph[“start”][“b”] = 2
这里写图片描述

print graph[“start”].keys() [“a”, “b”]
print graph[“start”][“a”] 2
>

让我们将剩余的节点和它们的邻居节点也添加到图中。 graph[“a”] = {}
graph[“a”][“fin”] = 1
graph[“b”] = {} graph[“b”][“a”] = 3 graph[“b”][“fin”] = 5
graph[“fin”] = {}
完整的图是长这样子的。
这里写图片描述
现在,我们需要一个Hash表来记录每条边的权值。权值记录的是从start节点到该节点需要的
这里写图片描述
时间。我们知道从start节点A需要6分钟,从start节点到节点B需要2分钟。我们不知道多久可 以到达finish节点。如果不知道权值,就写无穷大。Python中如何表示无穷大?
infinity=float(“inf”)
infinity=float(“inf”)
costs = {}
costs[“a”] = 6
costs[“b”] = 2
costs[“fin”] = infinity
我们还需要一张Parents表来存储节点和它的父节点。
这里写图片描述

这下面是如何存储数据的:
parents={}
parents[“a”] = “start”
parents[“b”] = “start”
parents[“fin”] = None
现在,我们需要一个数组,用来存储我们已经计算过的节点,因为我们只需要计算节点1 次。
processed = []
这些就是准备工作,现在我们来看一下算法的流程图
这里写图片描述
让我们先将代码写好,一会分析下流程。

node = find_lower_cost_node()
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_lower_cost_node()

这就是迪杰斯特拉算法。一会我会解释它。现在,我们先使用find_lower_cost_node方法。 首先,找到权值最小的节点,也就是节点B。
这里写图片描述

获得B节点的权值和它的邻居节点A和FIN。
这里写图片描述
对B的邻居节点进行遍历。
这里写图片描述
每个节点都有一个权值,权值即从起点(start)到达该节点所需要花费的时间。我们可以计
算从起点经过B点到达A点需要的时间。因为经过B点到达A点仅需要5分钟。
这里写图片描述
我们和它的邻居节点原有的权值进行比较
我们得到了到达A更近的路径,更新A的权值。
这里写图片描述
新的路径经过B。因此B是节点A的父节点,更新A的父节点。
这里写图片描述
现在,我们回到循环。B节点的另一个邻居节点是FIN。
这里写图片描述
通过B达到FIN(终点)需要多久呢?
这里写图片描述
需要7分钟,原来需要∞时间,7分钟与∞相比,
这里写图片描述
因此,更新FIN(终点)的权值和它的父节点。
这里写图片描述
现在,我们更新了B节点所有邻居节点的权值,将B节点存入process数组。
这里写图片描述
找到下一个便宜的节点(权值最小),也就是A节点。
这里写图片描述
节点A只有1个邻居极点,也就是FIN节点。
这里写图片描述
现在需要7分钟到达终点。若是经过A到达终点需要多长时间呢?
这里写图片描述

这里写图片描述

这个比原来的路径更快!更新它的权值和父节点。
这里写图片描述
当你处理完所有的节点时,算法也就结束了。我希望现在你对这个算法了解的很清楚了。找
到权值最小的节点这个功能比较简单,下面是代码:

def find_lowest_cost_node(costs):
    lowest_cost = float(“inf”)
    lowest_code_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

练习
7.1 在每幅图中,找到从起点到终点的最短路径?
这里写图片描述
概要
无权图适合用深度优先搜索算法;
迪杰斯特拉算法只适用于有权图;
迪杰斯特拉算法只适用于权值都是正数的图;
如果图中有父权值,需要使用Bellman-Ford(贝尔曼·福特)算法。

猜你喜欢

转载自blog.csdn.net/nfzhlk/article/details/79949785