有向图的最短路径问题(二)——Dijkstra算法

在有向图的最短路径问题(一)中我们介绍了可以求任意两点间最短距离的Floyd算法,虽然Floyd算法实现起来简单,但是其效率较低(O(n**3))。在本篇我们将介绍求某一点到图中所有点最短距离的算法——Dijkstra算法。

Dijkstra算法

image.png

使用Dijkstra算法求某一点到图中所有点最短距离有两种方法,分别使用的数据结构为邻接矩阵(python中以一个多重列表或矩阵表示)和邻接表(python中使用字典来表示)。但是无论使用那种数据结构,他的算法原理都是相同的,以上图为例,具体可分为下面四步:
(1)找出距离当前节点(即起点S)最近的节点,也即权重最小的边,前往该节点(即B);
(2)对于该节点(B)的邻居,较之已有的路径,检查是否有前往他们更短路径,如果有,就更新其开销(譬如SA=6,但是经过B点再到A的开销为5,小于6,更新);
(3)更新完所有邻居节点的开销后,又回到第(1)步,即选择距离当前节点最近的节点(已前往过的节点跳过),前往该节点并更新邻居的开销;循环这个过程,直到每个节点都走过;
(4)计算最终路径。

1、使用邻接矩阵

这里我们借用http://wiki.jikexueyuan.com/project/easy-learn-algorithm/dijkstra.html的例子,其有向加权图如下:

image.png

他的邻接矩阵e表示如下:
image.png

此外还需要初始化一个起始点到所有点的距离开销的数组(为方便我们命名为dis),后续我们更新的开销即为此数组中的开销:
初始距离

1)查找距离起点最近的点,即为dis中的最小的点,此处为dis[2]=1,即我们确定离起点最近的点一定为2;
2)接着我们更新点2相邻点<1,3,4>的开销,2_3=9, 2_4=3,然后我们比较经过2到达3,4的开销与1直接到达3,4(即dis[3],dis[4])的开销,我们发现dis[3]=12大于dis[2]+2_3=10,于是更新1到3的开销;同样的dis[4]=∞大于dis[2]+2_4=4,于是也更新dis[4]的开销为4(由于1为起点,所以这里不用考虑dis[1]与1_2+2_1的关系,虽然计算也是小于后者);更新完的dis数组如下所示:
更新点2相邻点

3)继续寻找剩余的点(3,4,5,6)中距离起点最近的点(此处为dis[4]=4,即确定了点4离点1的最近距离为4)来更新相邻点<2,3,5,6>的开销。dis[2]为已经确定了的,不用计算,虽然计算了也是不用更新。dis[3]=10大于dis[4]+4_3=8,于是更新其开销为8,同理更新dis[5]=17,dis[6]=19,结果如下:
更新点4相邻点

4)然后继续寻找剩下点(3,5,6)中的最小值,此处为3,也即确定了起点到点3的最近距离为dis[3]=8,然后更新其相邻点,结果为:
更新点3相邻点

5)继续寻找剩余的点(5,6)中的最小值,此处为5,即确定了起点到点5的最近距离为dis[5],更新相邻点结果为:
更新点5

6)最后确定6号点的最小值,更新其相邻点的开销(其实也没改变)。完成,1号点到所有点的最小值。
下面是上述例子的Dijkstra算法python实现代码:

# edges表示邻接矩阵
edges = [[0, 1, 12, 666, 666, 666],
         [666, 0, 9, 3, 666, 666],
         [666, 666, 0, 666, 5, 666],
         [666, 666, 4, 0, 13, 15],
         [666, 666, 666, 666, 0, 4],
         [666, 666, 666, 666, 666, 0]]
dis = {2: 1, 3: 12, 4: 666, 5: 666, 6: 666}
visited = []

min_dis = None
min_dis_point = None

for i in range(len(dis)):
    sort_dis = sorted(dis.items(), key=lambda item: item[1])
    # 找到dis中距离起始点距离最小的点
    for p, d in sort_dis:
        if p not in visited:
            min_dis_point = p
            min_dis = d
            visited.append(p)
            break
    for j in range(len(edges)):
        # 权重小于666的为相邻点
        if edges[min_dis_point-1][j] < 666:
            update = min_dis + edges[min_dis_point-1][j]
            # 若经过min_dis_point到j的距离比起点直达j的距离大,则更新
            if dis[j+1] > update:
                dis[j+1] = update

print(dis)

# 由于python中列表的索引是从0开始的,而我们的点是1-6,因此min_dis_point在邻接矩阵中的位置需要减一,
# 这里容易犯错,若是将点改为从0开始无论是写还是可读性都会好很多

2、使用邻接表

无论使用哪种数据结构,算法原理都是一样的,因此这里仅讨论使用邻接表时如何用Python代码实现。
由于python中没有指针,因此这里需要用嵌套字典来表示邻接表,形式为{节点:{相邻节点:连线权重}}

adj = {1: {2: 1, 3: 12}, 2: {4: 3, 3: 9}, 3: {5: 5}, 4: {3: 4, 5: 13, 6: 15}, 5: {6: 4}}
dis = {2: 1, 3: 12, 4: 666, 5: 666, 6: 666}
parents_node = {2: 1}
visited = []

min_dis = None
min_dis_point = None
for i in range(len(dis)):
    sort_dis = sorted(dis.items(), key=lambda item: item[1])
    # 找到dis中距离起始点距离最小的点
    for p, d in sort_dis:
        if p not in visited:
            min_dis_point = p
            min_dis = d
            visited.append(p)
            break
    try:
        # 更新相邻点的开销
        for n in adj[min_dis_point].keys():
            update = min_dis+adj[min_dis_point][n]
            if dis[n] > update:
                dis[n] = update
                parents_node[n] = min_dis_point
    except:  # 最后一个点6有进没出,因此没有相邻点会报错
        pass

travel = ''
for node, parents in parents_node.items():
    travel += str(parents)+'->'+str(node)+'->'

print(dis)
print(travel)

从某种程度来说使用邻接表可能更容易理解些,由于不用使用索引,书写代码时也没那么容易出错,同时由于不用储存那些不相邻的节点权重,也即邻接矩阵中的的那些无穷大的值,因此也可以节约内存。

你可能感兴趣的:(有向图的最短路径问题(二)——Dijkstra算法)