图的相关应用算法

要从哈尔滨坐火车到厦门需要如何买票,这个问题曾经困扰着我。排除买不买得到票的问题,哈尔滨到厦门之间没有直达车,需要找一个城市来中转,如何更快的回家?图的相关算法就可以解决这个问题。

单源顶点最短路径

单源顶点最短路径,什么意思?就是已知一个起点,求图上任意点的最短路径。这和之前的广度优先算法的目的类似,只不过广度优先算法考虑的是最少的边数,而单源顶点最短路径需要的是路径,二者在无权的前提下是一致的,但是一旦包含权值,二者的结果可能不一致。当然这里我们限制一下前提,这里解决的是有向带权图的单源顶点最短路径。

使用优先队列的Dijkstra算法

解决这个问题最常用的算法就是Dijkstra算法。使用优先队列的Dijkstra算法利用一个优先队列PQ(Priority Queue)的数据结构来实现功能。PQ通过二叉堆来实现将最小元素至于前部的功能,可以很方便给出最小元素。

import heapq
import sys
def buildHeap(A, position):
    for i in range(int(len(A) / 2) - 1, -1, -1):
        heapify(A, i, position)
def heapify(A, index, position):
    max = len(A)
    min = index
    left = 2 * index + 1
    right = 2 * index + 2
    if left < max and A[left] < A[index]:
        min = left
    if right < max and A[right] < A[min]:
        min = right
    if min != index:
        A[index], A[min] = A[min],A[index]
        position[A[index][1]] = index
        position[A[min][1]] = min
        heapify(A, min, position)
def up(A, index, position):
    parent = (index - 1) // 2
    if parent >= 0:
        if A[index] < A[parent]:
            A[index], A[parent] = A[parent], A[index]
            position[A[index][1]] = index
            position[A[parent][1]] = parent
            up(A, parent, position)
def pop(A, position):
    A[0], A[len(A)-1] = A[len(A)-1], A[0]
    position[A[0][1]] = 0
    position[A[len(A)-1][1]] = len(A)-1
    result = A.pop()
    position[result[1]] = -1
    if len(A) > 0:
        up(A, len(A)-1, position)
    return result
def DijkstraPQ(G, s):
    pred = [-1] * len(G)
    dist = [sys.maxsize] * len(G)
    dist[s] = 0
    pq = []
    position = []
    i = 0
    for d in dist:
        pq.append((d, i))
        position.append(i)
        i = i + 1
    buildHeap(pq, position)
    while pq:
        u = pop(pq, position)
        if G[u[1]]:
            for v in G[u[1]]:
                w = G[u[1]][v]
                newLen = dist[u[1]] + w
                if newLen < dist[v]:
                    decreaseKey(pq, position, v, newLen)
                    dist[v] = newLen
                    pred[v] = u[1]
    print(dist)
    print(pred)
def decreaseKey(A, position, v, newLen):
    A[position[v]] = (newLen, v)
    up(A, position[v], position)

这里采用DijkstraPQ来实现。首先对前序顶点pred和距离dist进行初始化,在pq中存入距离d和顶点编号构成的元组,利用之前讨论过的堆排序中的二叉堆(这里是顶小堆)来实现优先队列pq的功能。这里只需要能够有优先队列的两个功能,分别是取出最小元素pop和降低优先度decreaseKey

  • pop:pop的目的在于取出pq中的最小元素(取出后pq不包含该元素),这里由于二叉堆的性质,最小元素就在pq[0]。由于python的list.pop()在无参数时是O(1),而无参数是pop出尾部元素,所以这里先将首位元素调换,让后利用up让尾部元素上浮到恰当的位置。这样保证pop后的pq还满足二叉堆的要求。
  • decreaseKey:在我们找到比pq和dist中存储的距离更小的距离时需要将pq和dist中的距离更新。dist更新只需要重新赋值即可,而pq中距离的更新就比较麻烦,需要知道待更新距离的顶点在pq中的位置,还需要知道是否对二叉堆进行调整。对于顶点的位置,这里利用position来记录顶点的位置,如果顶点vkpop取出就在相应的position[k]赋值-1,其余的顶点vi在pq中的位置直接存储在对应的position[i]中。在找到顶点位置以后对相应位置i的pq[position[i]]进行更新。我们知道需要更新的距离小于原距离,所以利用up让更新的元素上浮到恰当的位置。
  • up比较好理解,就是将元素和父元素进行比较,小的在上,如果元素需要上浮就迭代调用up
  • buildHeapheapify参考之前的搜索与排序,在堆排序部分的二叉堆基础上将顶大堆改为顶小堆,并引入position实时记录pq中的位置变化。

DijkstraPQ不断取出堆顶部的元组u,u[1]所代表的顶点由二叉堆的性质可知是与起点距离dist最小的。对u[1]的邻接顶点v分别计算与起点s的距离,采用u[1]的距离dist[u[1]]加u[1]与v的距离G[u[1]][v]作为可能的最小距离newLen与dist[v]比较。如果newLen更小就将pred[v]保存为u[1],保存dist[v],更新pq中的元组。Dijkstra算法就是这样不断用距离最小的顶点作为新的起点来不断更新dist,实现对全部顶点与起点最小距离的计算。
考虑算法的时间复杂度可以发现初始化阶段为O(vlog2v),因为需要建二叉堆。后面我们实际遍历了每条边,不论是pop还是updecreaseKey,核心都是对二叉堆的调整,所以后面为O(elog2v)。合起来就是O((v+e)log2v)。其中v、e分别为顶点数和边数。

针对稠密图优化的Dijkstra算法

这里还有一种针对稠密图优化的Dijkstra算法。稠密图顾名思义就是边很多的图,具体而言可以说稠密图的边数e与顶点数的平方v2是同阶或以上的。

def DijkstraDense(G, s):
    pred = [-1] * len(G)
    dist = [sys.maxsize] * len(G)
    dist[s] = 0
    visited = [False] * len(G)
    while True:
        u = -1
        sd = sys.maxsize
        for i in range(len(G)):
            if not visited[i] and dist[i] < sd:
                sd = dist[i]
                u = i
        if u == -1:
            print(dist)
            print(pred)
            return
        visited[u] = True
        if G[u]:
            for v in G[u]:
                newLen = dist[u] + G[u][v]
                if newLen < dist[v]:
                    dist[v] = newLen
                    pred[v] = u

DijkstraDense多了一个visted来实现对已访问顶点的排除。通过寻找未被访问且距离最小的顶点u来实现之前优先队列的功能。时间复杂度也很好分析,while循环次数和顶点数v有关,而while内部每次都会对顶点进行遍历,此外实际上将while中所有有关于边的操作合并,恰好将边遍历,故为O(v2+e)。
这里需要交代的是,由于Dijkstra算法是一种贪心算法,所以在存在负权边时可能会出现问题。所以要求使用Dijkstra算法时不存在负权边。但是我们在实际的应用中会存在负权边的情况,这样就可以考虑使用Bellman-Ford算法了。

Bellman-Ford算法

Bellman-Ford算法可以在不存在负值环的情况下使用,事实上存在负值环本身就不存在最短路径。其原理就是不断的扫描整个图,记录与起点的最小距离,直到所有最短距离都获得,最短距离不更新为止。分析一种极端的情况,即某顶点需要由起点出发经由所有顶点才能到达,此时需要扫描len(G)-1次,说明至多len(G)-1次可以获得所有最短路径。此外,如果存在负值环,每次扫描都会有新的“最短”路径产生,所以在扫描len(G)-1次后若还存在新的最小路径,说明存在负值环。

def BellmanFord(G, s):
    pred = [-1] * len(G)
    dist = [sys.maxsize] * len(G)
    dist[s] = 0
    for i in range(len(G) + 1):
        failUpdate = (i == len(G))
        leaveEarly = True
        for u in range(len(G)):
            if G[u]:
                for v in G[u]:
                    newLen = dist[u] + G[u][v]
                    if newLen < dist[v]:
                        if failUpdate:
                            print("Has negative cycle")
                        dist[v] = newLen
                        pred[v] = u
                        leaveEarly = False
        if leaveEarly:
            print(dist)
            print(pred)
            return
    print(dist)
    print(pred)

BellmanFord中有两个bool量failUpdate、leaveEarly ,分别用来检测是否有负值环和是否可以提前结束。负值环检测的原理前面说了。提前结束的原理是新的一轮遍历没有更新距离,说明目前的距离已经是最短距离了。其他的部分比较简单就不赘述了。
Bellman-Ford算法的时间复杂度也很好分析总共v次扫描,每次扫描都对图进行遍历,即每次扫描e个边,显然是O(v*e)。

小结

我们之前给出了三种不同算法的时间复杂度。前面还介绍了一个概念叫稠密图,这里假设e和v2同阶,代入复杂度分别对于DijkstraPQDijkstraDenseBellmanFord是O((v2+v)log2v)、O(2v2)、O(v3)。可以发现针对稠密图设计的DijkstraDense时间复杂度是最小的,BellmanFord已经不堪用了。针对一个足够稀疏的图,假设e和v同阶,代入复杂度分别对于DijkstraPQDijkstraDenseBellmanFord是O(2vlog2v)、O(v2+v)、O(v2)。此时使用优先队列的DijkstraPQ成为时间复杂度最小的了。当然,BellmanFord也不是一无是处,BellmanFord在存在负权边情况下是三个算法中唯一的选择。

所有点对最短路径

与寻找单源不同,通常我们要找任意两个顶点之间的最短路径。要解决这个问题有两种思路,一种是直接对所有顶点实施一次单源顶点最短路径;一种是利用动态规划。
这里不介绍第一种方法,因为第一种方法效率不高而且结合前面的例子很容易实现。这里介绍利用动态规划的Floyd-Warshall算法。该算法最终会给出一个n阶方阵dist,dist[i][j]就是图中顶点vi、vj之间的最短距离。当然这里也是以有向带权图为输入的。

def FloydWarshell(G):
    dist = []
    pred = []
    for u in range(len(G)):
        dist.append([])
        pred.append([])
        for v in range(len(G)):
            dist[u].append(sys.maxsize)
            pred[u].append(-1)
        dist[u][u] = 0
        if G[u]:
            for v in G[u]:
                dist[u][v] = G[u][v]
                pred[u][v] = u
    for k in range(len(G)):
        for u in range(len(G)):
            if dist[u][k] == sys.maxsize:
                continue
            for v in range(len(G)):
                newLen = dist[u][k] +dist[k][v]
                if newLen < dist[u][v]:
                    dist[u][v] = newLen
                    pred[u][v] = pred[k][v]
    return pred

def ShortestPath(G, pred, s, e):
    if s < 0 or e < 0 or e >= len(G) or s >= len(G):
        return None
    print("Shortest Path from %d to %d is: " %(s, e), end = '')
    path = []
    sum = 0
    path.append(e)
    while e != s:
        temp = e
        e = pred[s][e]
        sum = sum + G[e][temp]
        if e == -1:
            return None
        path.append(e)
    path.reverse()
    print(path)
    print("Shortest distence is %d" %(sum))
    return path

FloydWarshell输入图G,先将dist和pred初始化,要求方阵dist对角元素为0,其余元素为最大或为已知的邻居顶点距离。pred为-1或存在邻居顶点时为自己。然后开始判断dist[u][k] +dist[k][v]是否小于dist[u][v],如果dist[u][k] +dist[k][v]小于dist[u][v]就将新的距离保存在dist[u][v],pred[u][v]实际就是pred[k][v]。通过不断解决这个小问题,直到dist[u][v]能够找到所有点对的最短距离。ShortestPath根据pred给出前序顶点给出最短路径。
FloydWarshell有三层循环嵌套,这个嵌套是O(v3)。ShortestPath就是将pred中的前序顶点列出,所以是O(e)。其中v是顶点数,e是边数。

最小生成树

之前都是讨论有向带权图的最短路径,现在再来考虑无向带权图的“最短路径”。给定一个图G,假设G中任意两个顶点可以直接连通或通过其他顶点连通,那么我们选出一组边满足连通所有顶点并且权值之和最小,这样的一组边就是最小生成树了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值