最短路(Floyd、Dijkstra、Bellman-Ford、队列优化的Bellman-Ford)

目录

    • 多源最短路 Floyd-Warshall
    • 单源最短路径 Dijkstra
    • 解决负权边 Bellman-Ford
    • Bellman-Ford的队列优化
    • 最短路算法对比

多源最短路 Floyd-Warshall

思路:如果要让任意两点ij之间的距离变短,只能引入第三个点k,通过这个顶点k中转即i->k->j,才可能缩短i到j的路程。Floyd算法就是每次增加一个允许通过的中转点,来求所有顶点的最短距离。

核心代码:

for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                if(a[i][k] < INF && a[k][j] < INF && a[i][k]+a[k][j] < a[i][j])
                    a[i][j] = a[i][k] + a[k][j];

这段代码的基本思想是:最开始只允许经过1号顶点进行中转,接下来只允许经过1号和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短距离。

时间复杂度 O ( N 3 ) O(N^3) O(N3),可以处理有负权边的图,不能处理负权回路。

单源最短路径 Dijkstra

用来求指定一个点(源点)到其他顶点的距离。

算法基本思想:每次找到离源点最近的一个顶点,然后以该顶点为中心进行扩展,最终得到源点到其余所有点的路径。步骤如下:

  • 将所有的顶点分为两部分:已知最短路径的顶点集合P和未知最短路径的顶点集合Q。最开始,已知最短路径的顶点集合P中只有源点一个顶点。用一个book数组来记录哪些点在集合P中,为1表示在P中,为0表示在Q中。

  • 设置源点s到自己的最短路径为0,即dis[s] = 0,若存在有源点能直接到达的顶点i,则把dis[i]设为e[s][i],同时把所有其他(源点不能直接到达的)顶点的最短路径设为 ∞ ∞

  • 在集合Q的所有顶点中选择一个离源点s最近的顶点u(即dis[u]最小)加入到集合P,并考察所有以点u为起点的边,对每一条边进行松弛操作。
    例如,存在一条从u到v的边,那么可以通过将边u->v添加到尾部来拓展一条从s到v的路径,这条路径的长度是dis[u]+e[u][v]。如果这个值比目前已知的dis[v]的值要小,就可以更新dis[v]=dis[u]+e[u][v]

  • 重复第3步,如果集合Q为空,算法结束,最终dis数组中的值就是源点到所有顶点的最短路径。

算法时间复杂度 O ( N 2 ) O(N^2) O(N2),找最近的那个顶点的时间复杂度 O ( N ) O(N) O(N),使用堆优化可以把这部分降到 O ( l o g N ) O(logN) O(logN),不能处理负权边,使用邻接表存储可以优化。

代码:

void dijkstra()
{
    //需要把集合Q中的n-1个顶点拿到P中
    for(int i = 1; i <= n-1; i++)
    {
        int minx = INF;
        int u;
        for(int j = 1; j <= n; j++)
        {
            if(book[j] == 0 && dis[j] < minx)
            {
                u = j;
                minx = dis[j];
            }
        }
        book[u] = 1;
        for(int v = 1; v <= n; v++)
        {
            if(dis[u] + a[u][v] < dis[v])
                dis[v] = dis[u] + a[u][v];
        }
    }
}

解决负权边 Bellman-Ford

核心代码只有四行,并可以完美解决带有负权边的图,还可以用来检测是否有负权回路

for(int k = 1; k <= n-1; k++) //进行n-1轮松弛
   for(int i = 1; i <= m; i++) //枚举每一条边
     if(dis[u[i]] != INF && w[i] != INF && dis[u[i]] + w[i] < dis[v[i]]) //尝试对每一条边进行松弛
         dis[v[i]] = dis[u[i]] + w[i];

最多进行n-1轮松弛。因为在一个含有n个顶点的图中,任意两点之间的最短路最多包含n-1条边。时间复杂度 O ( N M ) O(NM) O(NM)

Bellman-Ford算法还可以用来检测一个图是否含有负权回路,如果在进行n-1轮松弛之后,仍然存在

//尝试对每一条边进行松弛
if(dis[u[i]] != INF && w[i] != INF && dis[u[i]] + w[i] < dis[v[i]])     
   dis[v[i]] = dis[u[i]] + w[i];

的情况,说明在n-1轮松弛之后仍然可以继续松弛成功,那么此图必然存在负权回路。检测代码如下:

    for(int k = 1; k <= n-1; k++)
        for(int i = 1; i <= m; i++)
            if(dis[u[i]] + w[i] < dis[v[i]])
                dis[v[i]] = dis[u[i]] + w[i];
    int flag = 0;
    for(int i = 1; i <= m; i++)
    {
        if(dis[u[i]] + w[i] < dis[v[i]])
        {
            flag = 1;
            break;
        }
    }
    if(flag)
        printf("此图含有负权回路\n");

优化:如果已经松弛完毕,提前跳出循环,使用check变量来标记本轮是否发生了变化

for(int k = 1; k <= n-1; k++)
  {
       int check = 0;
       for(int i = 1; i <= m; i++)
       {
           if(dis[u[i]] + w[i] < dis[v[i]])
           {
               dis[v[i]] = dis[u[i]] + w[i];
               check = 1;
           }
       }
       if(!check)
           break;
}

Bellman-Ford的队列优化

在每实施一次松弛操作后,就会有一些顶点已经求得其最短路,此后这些顶点的最短路的估计值就会一直保持不变,不再受后续松弛操作的影响,但是上面代码还会继续判断是否需要松弛,这里浪费了时间。

这启发:每次仅对最短路估计值发生变化的顶点的所有出边执行松弛操作。

思路:每次选取队首顶点u,对顶点u的所有出边进行松弛操作,例如有一条u->v的边,如果dis[u]+e[u][v],且顶点v不在当前队列中,就将顶点v放入队尾。需要注意的是,同一个顶点同时在队列中出现多次毫无意义,所以需要一个数组来判重(判断哪些顶点已经在队列中)。在对顶点u的所有边松弛完毕后,就将顶点u出队。家下来不断从队列中取出新的队首顶点再进行如上操作,直至队列为空。

借助队列实现,代码如下(这里使用数组实现的邻接表):

#include 
#include 
#include 
#include 
using namespace std;

const int N = 51;
const int INF = 99;
int u[N], v[N], w[N], book[N];
int head, tail, dis[N], n, m;
int Queue[N], next[N], first[N];

void bellman_ford_()
{
    while(head < tail)
    {
        int t = Queue[head];
        int k = first[t];
        while(k != -1)
        {
            if(dis[u[k]] + w[k] < dis[v[k]])
            {
                 dis[v[k]] = dis[t] + w[k];
                 if(book[v[k]] == 0)
                 {
                     Queue[tail] = v[k];
                     tail++;
                     book[v[k]] = 1;
                 }
            }
            k = next[k];
        }
        book[Queue[head]] = 0;
        head++;
    }
}

int main()
{
    memset(book, 0, sizeof(book));
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++)
    {
        dis[i] = INF;
        first[i] = -1;
    }
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d%d", &u[i], &v[i], &w[i]);
        next[i] = first[u[i]];
        first[u[i]] = i;
    }
    dis[1] = 0;
    head = tail = 0;
    Queue[tail++] = 1;
    book[1] = 1;
    bellman_ford_();
    for(int i = 1; i <= n; i++)
        printf("%d ", dis[i]);
    return 0;
}

/*
5 7
1 2 2
1 5 10
2 3 3
2 5 7
3 4 4
4 5 5
5 3 6
*/

需要说明:

  • book[Queue[head]] = 0; 这句的理解
    通常一个顶点出队后不会再重新进入队列,而这里一个顶点很可能在出队列之后再次放入队列,也就是当一个顶点的最短路程估计值变小之后,需要对其所有的出边进行松弛,但是如果这个顶点的最短路程估计值再次变小,仍需要对其所有的出边再次进行松弛,这样才能保证相邻顶点的最短路程估计值同步更新。

  • Bellman-Ford队列优化如何判断一个图是否有负环?
    某个点进入队列的次数超过n次。

最短路算法对比

Floyd Dijkstra Bellman-Ford 队列优化的Bellman-Ford
空间复杂度 O ( N 2 ) O(N^2) O(N2) O(N^2),使用邻接表、堆优化 O ( M ) O(M) O(M) O ( M ) O(M) O(M) O ( M ) O(M) O(M)
时间复杂度 $O(N^3) O ( N 2 ) O(N^2) O(N2),使用邻接表、堆优化最少 O ( ( M + N ) l o g N ) O((M+N)logN) O((M+N)logN) O ( N M ) O(NM) O(NM) 最 坏 O ( N M ) 最坏O(NM) O(NM)
适用情况 稠密图 稠密图 稀疏图,和边关系密切 稀疏图,和边关系密切
有负权边 可以 不可以 可以 可以
判定是否存在负权回路 不能 不能 可以判定 可以判定

你可能感兴趣的:(基本算法,编程练习,C/C++,啊哈!算法,练习,最短路,算法)