6
狄克斯特拉算法是一种实现了在有障碍物的两个地点之间找出一条最短路径的高效算法,解决了机器人学中的一个十分关键的问题,即运动路径规划问题,至今仍被广泛应用。是“贪心方法(greedy method)”的一个成功范例。

致敬

首先向伟大的牛人致敬!

图片描述

使用狄克斯特拉算法

如果所示:图片描述

找出从起点到终点耗时最短的路径。其中,每个数字表示的都是时间,单位分钟。线路使用有方向的箭头线路表示,表示只有一个方向可以使用,统称有向图

常规思路

为了找到从起点到终点耗时最短的路径,我们通常需要找出从起点到终点所有的路径,然后进行一一对比。

路径一

图片描述

路径一是从起点出发,经过 A 节点,到达终点,共计用时 6 + 1 = 7 分钟。

路径二

图片描述

路径二是从起点出发,经过 B 节点,到达终点,共计用时 2 + 5 = 7 分钟。

路径三

图片描述

路径三从起点出发,经过 B 节点,再经过 A 节点,到达终点,共计用时 2 + 3 + 1 = 6 分钟。

综上,我们已经穷举完了所有可能的路径,不可能再找出另外的路径了。同时,对比一下三种路径,你会发现路径三用时最短,只需要 6 分钟。呵呵,so easy,妈妈再也不用担心我的学习了。既然,人可以做出结果,那么计算机利用此种方法,也就是所谓的穷举法,当然也能找到最优路径。

不过,别得意,你的妈妈还得担心你的学习。如果路途中,不止 A、B 两个节点,而是接近无穷多个节点,记住是接近无穷多个节点,......懵逼从天空飘过。

此时,肯定有同学会跳出来反对,无穷多个节点,这就是无限。无限,也就是无界,就是死循环的问题了,肯定无法得到答案,此题出得有问题。

这个问题提得好,必须有界才能有答案,该问题是接近无限多,也就是个很大很大的边界,是超出了人力范围的边界。......懵逼继续从天空飘过。 但是,计算机肯定是能够计算出有界的问题的,利用穷举法当然可以算出,不过这里又产生一个问题,穷举法是检索每条可能的路径,这肯定会消耗很大的计算机运算能力,那么有没有更优的方法,至少不用穷举出所有路径的方法呢?当然,有那么多的牛人供我们致敬,答案是肯定的。

狄克斯特拉算法思路

步骤或思路如下:

  1. 找出最便宜的节点,即可在最短时间内前往的节点。
  2. 对于该节点的所有邻居,检查是否有前往它们的更短路径,如果有,就更新其开销。
  3. 处理过的节点,进行标记,以后将不再处理。
  4. 重复以上过程,直到对图中的每个节点都这样做了。
  5. 计算出最终路径。

第一步:找出最便宜的节点。你站在起点,不知道该前往节点 A 还是节点 B ,茫然无措ing........。此时,散列表可以派上用场了。啥是散列表?你可以把它当做是一个列表,详细的东西问谷歌,请自备梯子

前往节点 A 需要6 分钟,而前往节点 B 需要 2 分钟。至于前往其它节点,你还不知道需要多长时间。那么散列表如下:

父节点 节点 耗时
起点 A 6
起点 B 2
起点 终点

第二步:由于从起到到节点 B 的路径耗时少,先计算经节点 B 前往其各个邻居所需的时间。

父节点 节点 耗时
B A 5 更新耗时
- B 2
B 终点 7 更新耗时

这一步,找到了一条前往节点 A 的更短路径!直接前往节点 A 需要 6 分钟。但是经过节点 B 前往节点 A 只需要 5 分钟。

对于节点 B 的邻居,如果找到前往它的更短路径,就更新其开销。

  • 前往节点 A 的更短路径,时间从 6 分钟缩短到 5 分钟。
  • 前往终点的更短路径,时间从无穷大缩短到 7 分钟。

第三步:对节点 B 已进行处理,所以要对节点 B 进行标记,以后将不再处理节点 B。

第四部: 重复!

重复第一步:找出可在最短时间内前往的节点。除节点 B 之外,可以在最短时间内前往的节点是节点 A 。
重复第二步:更新节点 A 的所有邻居的开销。

父节点 节点 耗时
- A 5 已是最小耗时,无需更新
- B 2
A 终点 6 更新耗时

现在对每个节点都运行了狄克斯特拉算法(无需对终点这样做)。现在,你知道:

  • 前往节点 B 需要 2 分钟;
  • 前往节点A 需要 5 分钟;
  • 前往终点需要 6 分钟。

所以你会发现,前往终点的时间为 6 分钟!!!

Python代码实现

实现一个能够找出开销最低节点的函数

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

创建用于存储所有节点及其前往邻居开销的散列表代码

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"] = {} # 终点没有任何邻居

表示整个图的散列表类似下面这样。

父节点 节点 耗时
起点 A 6
起点 B 2
A 终点 1
B A 3
B 终点 5
起点 终点 -

按流程实现代码

一、创建从起点开始的开销表代码如下:

infinity = float("inf")
costs = {}
costs["a"] = 6
costs["b"] = 2
costs["fin"] = infinity

二、创建存储父节点的散列表代码如下:

parents = {}
parents["a"] = "start"
parents["b"] = "start"
parents["fin"] = None

三、创建一个数组,用于记录处理过的节点,对于同一个节点,不用多次处理。

processed = []

四、按照算法列出代码

node = find_lowest_cost_node(costs) # 在未处理的节点中找出开销最小的节点
while node is None: # 这个 while 循环在所有节点都被处理过后结束
    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_lowest_cost_node(costs) # 找出接下来要处理的节点,并循环。

按照个人的理解创建代码

在 Python 3.5.2 上亲测有效。

# -*- coding:utf-8 -*-
__author__ = "东方鹗"
__blog__ = "www.os373.cn"

def find_lowest_cost_node(costs, processed):
    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


def best_route():
    ''' 存储所有节点及其下一个节点开销的字典 '''
    graph = {"start": {"a": 6, "b": 2}, "a": {"fin": 1}, "b": {"a": 3, "fin": 5}, "fin": {}}

    ''' 从起点开始,包含所有下一个节点开销的字典 '''
    infinity = float("inf")
    costs = {"a": 6, "b": 2, "fin": infinity}

    ''' 从起点开始,存储所有父节点的散列表 '''

    parents = {"a": "start", "b": "start", "fin": None}
    best_route = ""
    processed = []

    node = find_lowest_cost_node(costs, processed) # 在未处理的节点中找出开销最小的节点    
    while node is not None: # 这个 while 循环在所有节点都被处理过后结束
        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_lowest_cost_node(costs, processed) # 找出接下来要处理的节点,并循环。
    
    p = parents["fin"]
    
    while True:  
        best_route += "%s<——" % p
        p = parents[p]
        
        if p is "start":
            break               
        
                
    return "到达终点的最终路径是: 终点<——%s起点。\n最快到达的时间是%s分钟" % (best_route, costs["fin"])
        
if __name__ == "__main__":
    best_route = best_route()
    print(best_route)

结果如下:

到达终点的最终路径是: 终点<——a<——b<——起点。
最快到达的时间是6分钟

狄克斯特拉算法的局限

1、该算法只适用于有向无环图!
2、该算法将 0 视作最小的权值,也就是说,如果出现负权值的情况,那么该算法将失效!

reference


藕丝空间
1.7k 声望271 粉丝

宗旨:致力于在新乡本地传播编程知识。