学习笔记
学习书目:《算法图解》- Aditya Bhargava



文章目录



狄克斯特拉算法



在上一个Blog中,我们用广度优先搜索找到了从家到公园换乘最少的路线,即家–1-->路–>A–3路–>E–5路–>公园。

小白的算法初识课堂(part7)--狄克斯特拉算法_父节点



但是有的时候,我们要寻找的是从家到公园耗时最短的路线,这时广度优先搜索就不顶用了,我们将会使用狄克斯特拉算法解决这个问题。

我养了一直兔子,它叫小黄,我在家里给小黄从它的窝到餐厅搭一个兔子专用通道:

小白的算法初识课堂(part7)--狄克斯特拉算法_算法_02

假设小黄从始至终保持匀速前进,且到达一个地点不做停留。

试问,从窝到餐厅,小黄走哪条路线耗费时间最短?在这种情境中,这个问题也可以理解为,小黄走哪条路线的总路程最短?

如果用广度优先搜索,小黄可能会选窝–>厕所–>餐厅或者窝–>客厅–>餐厅,但是这两条路线所的总长为7m,并不是最短路线。

下面我们用狄克斯特拉算法寻找最短路线。狄克斯特拉算法包含4个步骤:

(1) 找出“最便宜”的节点,即可在最短时间内到达的节点。

(2) 更新该节点的邻居的开销,其含义将稍后介绍。

(3) 重复这个过程,直到对图中的每个节点都这样做了。

(4) 计算最终路径。



具体步骤实现



  • 第一步:找到最便宜的节点

现在,小黄蹲在自己的窝里,它前往客厅要2m,前往厕所要6m,至于其他节点,小黄现在不知道要多远,对于这种不知道路程长短的节点,小黄先设置其长度为无穷大小白的算法初识课堂(part7)--狄克斯特拉算法_父节点_03

节点

长度

客厅

2

厕所

6

餐厅

我们看到客厅离小黄最近,前往客厅只需要2m。

  • 第二步:计算由客厅前往其他邻居所需的路程

节点

长度

客厅

2

厕所(更新)

5

餐厅(更新)

7

小黄找到了一条前往厕所更短的路,只需要5m;同时,它也找到了前往餐厅更短的路,只需要7m。因为我们找到了前往厕所和餐厅更短的路,所以我们在表中更新其开销。

  • 第三步:重复

重复第一步:找出除客厅节点以外的,可在最短路程内前往的节点。小黄观察到,除了客厅以外,可在最短路程内前往的节点是厕所。

重复第二步:更新厕所节点所有邻居的开销。

节点

长度

客厅

2

厕所

5

餐厅(更新)

6

更新后,小黄发现,前往餐厅只需要6m了。

小黄对每个节点都运行了狄克斯特拉算法(不需要对终点这样做),现在它知道了:

  • 前往客厅节点只需要2m
  • 前往厕所节点只需要5m
  • 前往餐厅节点只需要6m

现在,小黄知道,最短路径是从窝–>客厅–>厕所–>餐厅,总路程只要6m.

上一个Blog里,使用了广度优先搜索来查找两点之间的最短路径,那时“最短路径”的意思是段数最少。在狄克斯特拉算法中,小黄给每段都分配了一个数字或权重,因此狄克斯特拉算法找出的是总权重最小的路径。



术语



狄克斯特拉算法用于每条边都有关联数字的图,这些数字称为权重。带权重的图称为加权图,不带权重的图称为非加权图

小白的算法初识课堂(part7)--狄克斯特拉算法_python_04



要计算非加权图中的最短路径,可使用广度优先搜索。要计算加权图中的最短路径,可使用狄克斯特拉算法。图还可能有环,这意味着我们可从一个节点出发,走一圈后又回到这个节点。

小白的算法初识课堂(part7)--狄克斯特拉算法_算法_05

值得注意的是,无向图意味着两个节点彼此指向对方,其实就是环。在无向图中,每条边都是一个环。狄克斯特拉算法只适用于有向无环图。



跳蚤市场



假设我们小时候都参加过跳蚤市场,我们知道跳蚤市场中非常自由,大家可以以物换物,直接用钱购买物品,甚至是以钱+物换物的方式进行交易。

现在,我有本《数学之美》,我可以有以下交易选项:

小白的算法初识课堂(part7)--狄克斯特拉算法_编程之美_06

上图说的是,如果我想得到《编程之美》可以用《数学之美》去换,且不用多花一分钱;而得到《西瓜书》则要用《数学之美》再加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元加《编程之美》换取西瓜书:

小白的算法初识课堂(part7)--狄克斯特拉算法_狄克斯特拉算法_07



那么现在我有两种选择可以得到《编程之美》:

(1)不花一分钱换《编程之美》

(2)先花5元买《西瓜书》,再和A同学交易《编程之美》,那么我们将得到《编程之美》加2元

显然方法2更好一些。

虽然我们可以这样做,但是狄克斯特拉算法并不支持这样的方式,如果有负权边,就不能使用狄克斯特拉算法。因为负权边会导致这种算法不管用。

这是因为狄克斯特拉算法这样假设:对于处理过的《编程之美》节点,没有前往该节点的更短路径。这种假设仅在没有负权边时才成立。因此,不能将狄克斯特拉算法用于包含负权边的图。在包含负权边的图中,要找出最短路径,可使用另一种算法—贝尔曼-福德算法.

看不懂上面这句话没关系,我们按照狄克斯特拉算法的流程画两个表格:

父节点

节点

开销

《数学之美》

《编程之美》

0

《数学之美》

《西瓜书》

5


PS4手柄

找到最便宜节点《编程之美》,更新其邻居的开销:

父节点

节点

开销

《数学之美》

《编程之美》

0

《数学之美》

《西瓜书》

5

《编程之美》

PS4手柄(更新)

35

找到最便宜节点《西瓜书》,更新其邻居的开销:

父节点

节点

开销

《数学之美》

《编程之美》(已用)(更新)

-2

《数学之美》

《西瓜书》

5

《编程之美》

PS4手柄

35

我们看到《编程之美》已经被我们处理过了,这里却对其进行了更新。在狄克斯特拉算法中这非常危险,因为节点一旦被处理,就意味着没有前往该节点的更便宜的路径,但是我们却找到了更便宜的路径。



python实现



我们以下面这幅有向图为例,用python实现狄克斯特拉算法:

小白的算法初识课堂(part7)--狄克斯特拉算法_狄克斯特拉算法_08

要编写解决这个问题的代码,需要三个散列表:

小白的算法初识课堂(part7)--狄克斯特拉算法_父节点_09

随着算法的进行,我们还要不断的更新散列表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)