最短路径

最短路径问题总结。

最短路径问题

Dijkstra

适用于单源点、非负权值的图。

基本思想是基于贪心法,执行过程描述如下:

初始化字典dist表示源点到任一顶点的距离。dist[源点]=0,其余INF。然后用一个集合set保存已经被访问的元素。从源点开始,对该顶点所有的出边进行松弛,然后加入到被访问集合中,然后在dist字典中寻找下一个值最小的顶点,依次进行直到所有顶点已经加入被访问集合中。

以上是最朴素的思想,可以用一个优先级队列进行优化。

代码实现如下:

def Dijkstra(G, vo):
    old = set()
    view = vo

    INF = float('inf')

    dis = dict((k, INF) for k in G.keys())  # dis[k]表示到k的最短距离
    dis[vo] = 0

    path={}

    while len(old) < len(G):
        old.add(view)  # 当前考虑的节点
        for w in G[view]:#遍历view周围的节点,进行松弛
            if dis[w] > dis[view] + G[view][w]:
                dis[w] = dis[view] + G[view][w]
                path[w]=view#path[w]记录w的上一个结点的下标

        new =INF

        for n in dis.keys():#找下一个节点k,要求dis[k]最小且k未被松弛过。
            if n in old:
                continue
            if dis[n]<=new: #必须小于等于,因为如果一个顶点没出边,for循环转一圈没有意义,view会一直是这个顶点.
                view=n
                new=dis[n]

    for x in path:
        print('到顶点',x,'的路径为',end=': ')
        t=x
        while t!=None:
            print(t,"<-",end=' ')
            t=path.get(t)
        print('总长度为',dis[x])

    return dis,path

这里G代表图,vo代表起点。图的表示使用嵌套的字典表示的,即{‘A’:{‘B’:3}}表示顶点A有A到B的边,权值为3.

Bellman-Ford

适用于单源非负环图。

思想描述如下:

初始化dist字典同上,松弛每条边(等价于松弛所有顶点)顶点-1轮,如果之后松弛还能成功,表明有负环存在,无法求解。

def Bellman_Ford(G,v):
    '''
    每一轮对所有顶点进行松弛,如果哪一轮松弛后最短距离都不变化就提前结束。否则,如果执行n
    轮之后还在变化则说明存在负环。最多n-1次松弛就可以得到答案。(n为顶点个数)
    :param G: 图的邻接表
    :param v: 起点
    :return: dis,path
    '''
    dis=dict((k,float('inf'))for k in G)
    dis[v]=0
    path={}
    for rounds in G:#一共执行n次,如果n次之后最短路径还没求出来说明有负环
        changed=False
        #对所有顶点进行松弛
        for u in G:
            for w in G[u]:
                if dis[w] > dis[u] + G[u][w]:
                    dis[w] = dis[u] + G[u][w]
                    path[w] = u
                    changed=True
        if not changed:
            break
    else:
        sys.exit('存在负环,算法求不出来')
    return dis,path

SPFA

用队列优化的Bellman-Ford算法,考虑上述算法,显然只有前面的边松弛成功,后面的边松弛才会成功。所以上面的实现用了个标志用于提前结束,这里用一个队列,把松弛成功的顶点的所有邻接顶点加入其中,每次从队列取出顶点进行松弛。

def SPFA(G,v):
    '''
    利用 spfa 算法判断负环有两种方法:

      1) spfa 的 dfs 形式,判断条件是存在一点在一条路径上出现多次。

      2) spfa 的 bfs 形式,判断条件是存在一点入队次数大于总顶点数。
    :param G: 图
    :param v: 起点
    :return: dis,path
    '''
    #用队列优化Bellman-Ford算法,这里不能有负环,程序中没有判断。
    q=deque()
    path={}#用于打印路径
    dis=dict((k,float('inf'))for k in G)#记录距离
    dis[v]=0
    times=dict((k,0) for k in G)#用于记录每个顶点入队列的次数,如果大于顶点个数表明有负环
    q.append(v)#起始把起点入队
    while len(q)!=0:
        x=q.popleft()#出队一个
        #对该顶点所有的邻边进行松弛
        for w in G[x]:
            if dis[w]>dis[x]+G[x][w]:
                dis[w]=dis[x]+G[x][w]
                path[w]=x #记录此顶点的上一个顶点
                if w not in q:
                    if times[w]>len(G):
                        sys.exit('存在负环,嗝屁了')
                    q.append(w)
                    times[w]+=1
    return dis,path

Floyd-Warshall

基于DP的一个算法,代码即是递推式。

思想就是u到v的最短路径经不经过k,这也解释了为什么k是最外层循环(自底向上运算,后面的需要用到前面的结果)

def Folyd_Warshall(G):
    G2=deepcopy(G)
    #强行补成邻接矩阵的形式
    for u in G2:
        for v in G2:
            if u==v:
                G2[u][v]=0
            if G2[u].get(v)==None:
                G2[u][v]=float('inf')
    #算法开始
    for k in G:
        for u in G:
            for v in G:
                if G2[u][v]>G2[u][k]+G2[k][v]:
                    G2[u][v]=G2[u][k]+G2[k][v]
    return G2

Johnson

这个算法利用了Bellman和Dijkstra算法。

由于Bellman算法限制,同样不能用于负环图

其本质思想是:

给图加上一个顶点s,然后令s到其所有顶点距离为0,利用Bellman-Ford算法,求出s到其它顶点的最短距离记为h(h[a]表示s到a的最短距离),然后对图中所有的边进行重新赋权(目的消除负权,好使用Dijkstra算法),具体体现为若有顶点u、v和边G[u][v],则G[u][v]+=h[u]-h[v]。之后,去除顶点s即可。然后对所有顶点执行Dijkstra算法。最后在已经求解的结果上别忘了恢复原来的权值,即Dist[u][v]+=h[v]-h[u],这里Dist表示u到v的最短距离。

可以证明,重新赋权图求得的最短路径和原始图一样,(值不一样可以通过恢复操作来求出)。

def johnson(G):
    G=deepcopy(G)
    s=object()
    G[s]={v:0 for v in G} #新节点到所有其它顶点距离为0
    h,_=Bellman_Ford(G,s) #h表示s到其它节点的最短路径值
    del G[s] #可以删除s节点了
    for u in G:  #对于从u到v的边,进行重新赋权
        for v in G[u]:
            G[u][v]+=h[u]-h[v]
    D={}
    for u in G:
        D[u]=Dijstra(G,vo=u)[0]
        for v in G:
            D[u][v]+=h[v]-h[u]
    return D

附上测试用例和打印路径的函数

def print_path(dis,path):
    for x in path:
        print('到顶点',x,'的总长度为:',dis[x],'具体的路径为',end=': ')
        t=x
        while t!=None:
            print(t,"<-",end=' ')
            t=path.get(t)
        print()

      G1={'B': {'D': 3, 'E': 0, 'C': 1},
        'D': {'B': 0, 'C': 2},
        'E': {'D': 0},
        'C': {},
        'A': {'B': 1, 'C': 3}}

    G={0:{1:5,2:3},
       1:{0:5,3:1,4:3,5:6},
       2:{0:3,4:8,5:7,6:6},
       3:{1:1,7:6,8:8},
       4:{1:3,2:8,7:3,8:5},
       5:{1:6,2:7,8:3,9:3},#5
       6:{2:6,8:8,9:4},#6
       7:{3:6,4:3,10:2,11:2},#7
       8:{3:8,4:5,5:3,6:8,11:1,12:2},#8
       9:{5:3,6:4,11:3,12:3},#9
       10:{7:2,13:3,14:5},#10
       11:{7:2,8:1,9:3,13:5,14:2},#11
       12:{8:2,9:3,13:6,14:6},#12
       13:{10:3,11:5,12:6,15:4},#13
       14:{10:5,11:2,12:6,15:3},#14
       15:{13:4,14:3}#15
       }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值