带权最短路 Dijkstra, SPFA, Bellman-Ford, ASP, Floyd-Warshall 算法分析

图论中,用来求最短路的方法有很多,适用范围和时间复杂度也各不相同。

本文主要介绍的算法的代码主要来源如下:

  1. Dijkstra: Algorithms(《算法概论》)Sanjoy Dasgupta, Christos Papadimitriou, Umesh Vazirani;《算法竞赛入门经典—训练指南》刘汝佳、陈峰。
  2. SPFA (Shortest Path Faster Algorithm): Data Structure and Algorithms Analysis in C, 2nd ed.(《数据结构与算法分析》)Mark Allen Weiss.
  3. Bellman-Ford: Algorithms(《算法概论》)Sanjoy Dasgupta, Christos Papadimitriou, Umesh Vazirani.
  4. ASP (Acyclic Shortest Paths): Introduction to Algorithms - A Creative Approach(《算法引论—一种创造性方法》)Udi Manber.
  5. Floyd-Warshall: Data Structure and Algorithms Analysis in C, 2nd ed.(《数据结构与算法分析》)Mark Allen Weiss.

它们的使用限制和运行时间如下:

Dijkstra: 不含负权。运行时间依赖于优先队列的实现,如 O((∣V∣+∣E∣)log∣V∣)O((∣V∣+∣E∣)log∣V∣)
SPFA: 无限制。运行时间O(k⋅∣E∣) (k≪∣V∣)O(k⋅∣E∣) (k≪∣V∣)
Bellman-Ford:无限制。运行时间O(∣V∣⋅∣E∣)O(∣V∣⋅∣E∣)
ASP: 无圈。运行时间O(∣V∣+∣E∣)O(∣V∣+∣E∣)
Floyd-Warshall: 无限制。运行时间O(∣V∣3)

其中 1~4 均为单源最短路径 (Single Source Shortest Paths) 算法; 5 为全源最短路径 (All Pairs Shortest Paths) 算法。顺便说一句,为什么没有点对点的最短路径?如果我们只需要一个起点和一个终点,不是比计算一个起点任意终点更节省时间么?答案还真不是,目前还没有发现比算从源点到所有点更快的算法。

图的表示

本文中,前四个算法的图都采用邻接表表示法,如下:

struct Edge
{
    int from;
    int to;
    int weight;

    Edge(int f, int t, int w):from(f), to(t), weight(w) {}
};

int num_nodes;
int num_edges;
vector<Edge> edges;
vector<int> G[max_nodes]; // 每个节点出发的边编号
int p[max_nodes]; // 当前节点单源最短路中的上一条边
int d[max_nodes]; // 单源最短路径长

Dijkstra 方法

Dijkstra 方法依据其优先队列的实现不同,可以写成几种时间复杂度不同的算法。它是图论-最短路中最经典、常见的算法。关于这个方法,网上有许多分析,但是我最喜欢的还是《算法概论》中的讲解。为了理解 Dijkstra 方法,首先回顾一下无权最短路的算法。无权最短路算法基于 BFS,每次从源点向外扩展一层,并且给扩展到的顶点标明距离,这个距离就是最短路的长。我们完全可以仿照这个思路,把带权图最短路问题规约到无权图最短路问题——只要把长度大于 1 的边填充进一些「虚顶点」即可。如下图所示。

Dijkstra

这个办法虽然可行,但是显然效率很低。不过,Dijkstra 方法EC,EB,ED分别出发,经过一系列「虚节点」,依次到达D,B,C 。为了不在虚节点处浪费时间,出发之前,我们设定三个闹钟,时间分别为4,3,2提醒我们预计在这些时刻会有重要的事情发生(经过实际节点)。更一般地说,假设现在我们处理到了某个顶点u,和u相邻接的顶点为v1,v2,…,vn,它们和uu的距离为d1,d2,…,dn。我们为v1,v2,…,vn各设定一个闹钟。如果还没有设定闹钟,那么设定为d ;如果设定的时间比d晚,那么重新设定为d(此时我们沿着这条路比之前的某一条路会提前赶到)。每次闹钟响起,都说明可能经过了实际节点,我们都会更新这些信息,直到不存在任何闹钟。综上所述,也就是随着 BFS 的进行,我们一旦发现更近的路径,就立即更新路径长,直到处理完最后(最远)的一个顶点。由此可见,由于上述「虚顶点」并非我们关心的实际顶点,因此 Dijkstra 方法的处理方式为:直接跳过了它们。

还需要解决的一个问题,就是闹钟的管理。闹钟一定是从早到晚按顺序响起的,然而我们设闹钟的顺序却不一定按照时间升序,因此需要一个优先队列来管理。Dijkstra 方法实现的效率严重依赖于优先队列的实现。一个使用标准库容器适配器 priority_queue 的算法版本如下:

typedef pair<int, int> HeapNode;
void Dijkstra(int s)
{
    priority_queue< HeapNode, vector<HeapNode>, greater<HeapNode> > Q;
    for (int i=0; i<num_nodes; ++i)
        d[i] = __inf;

    d[s] = 0;
    Q.push(make_pair(0, s));
    while (!Q.empty()) {
        pair<int, int> N = Q.top();
        Q.pop();
        int u = N.second;
        if (N.first != d[u]) continue;
        for (int i=0; i<G[u].size(); ++i) {
            Edge &e = edges[G[u][i]];
            if (d[e.to] > d[u] + e.weight) {
                d[e.to] = d[u] + e.weight;
                p[e.to] = G[u][i];
                Q.push(make_pair(d[e.to], e.to));
            }
        }
    }
}

Bellman-Ford:一个简单的想法

Dijkstra 方法的本质是进行一系列如下的更新操作

d(v)=min{ d(v), d(u)+l(u,
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值