当前位置:   article > 正文

单源最短路问题——BellmanFord算法和SPFA算法详解

单源最短路问题——BellmanFord算法和SPFA算法详解

单源最短路

1. 问题建模:

已知源点st,有向图G(无向图的每条边被视为两条有向边),求st到图中各个点的最短距离。

利用dis数组表示源点st到各个点的最短距离,初始时dis[st] = 0,其他为INF。

2. 理论基础:松弛

边的松弛:边a->b长度为c,如果dis[b]>dis[a] + c那么可以将dis[b]更新为dis[a] + c。

点的松弛:对以点v为起点的所有边进行边的松弛。

证明: 当每一条边都无法使用松弛操作更新dis数组时,dis数组达到最优。

在这里插入图片描述


其实所有的单源最短路算法都是以不同的顺序进行松弛操作,松弛操作的顺序也决定了算法的速度,如:Dijkstra算法的松弛顺序优于Bellman-Ford,也就是说Dijkstra算法可以以更少的松弛操作次数求得最优的dis数组。


3. 最短路变体: 不同的路径长度表示方法

需要证明:下列三种路径长度表达方式都能满足:“松弛的不能再松弛”的状态 与 dis数组达到最优互为充要条件

3.1 路径长度为权值乘积(要求权值C > 0)

// 边起点为a 终点为b 权值为c    
定义放松操作为: 
    if (dis[b] > dis[a] * c)
        dis[b] = dis[a] * c
证明:当放松到无法放松时,dis数组最优
必要性:当dis数组最优时,如果还可以执行放松操作那么dis数组的值会变小,
       这与dis数组最优矛盾。
// V为点V(i-1)->Vi的权值为Ci
// V1为源点
充分性:对于任意一条最短路:V1->V2->V3-> .... V(k-1)->Vk有:
           dis[Vk] <= dis[V(k-1)] * Ck
           dis[V(k-1)] <= dis[V(k-2)] * C(k-1)
           :
           :
           dis[V3] <= dis[V2] * C3
           dis[V2] <= dis[V1] * C2
       将上述不等式相乘有:dis[Vk] <= Ck * C(k-1) * ... * C2 = 最优路径
       已知权值路径为最短路所以:dis[Vk] >= 最优路径
       所以dis[Vk] = 最优
得证。     
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

3.2 路径长度为权值最大值

最大值不等于最长路,最小值也不是最长路,这只是路径长度的一种表示方式,换句话说我们求得是某种路径长度表示下的最短路(例如:路径长度为权值最大值时,我们求得是让最大值最小)

// 边起点为a 终点为b 权值为c  不要求权值C > 0 
定义放松操作为: 
    if (dis[b] > max(dis[a], c))
        dis[b] = max(dis[a], c)
证明:当放松到无法放松时,dis数组最优
必要性:当dis数组最优时,如果还可以执行放松操作那么dis数组的值会变小,
       这与dis数组最优矛盾。
// V1, V2, ..... , V(k-1), Vk为点  
// V(i-1)->Vi的权值为Ci
// V1为源点
充分性:对于任意一条最短路:V1->V2->V3-> .... V(k-1)->Vk有:
           dis[Vk] <= max(dis[V(k-1)], Ck)
           dis[V(k-1)] <= max(dis[V(k-2)], C(k-1))
           :
           :
           dis[V3] <= max(dis[V2], C3)
           dis[V2] <= max(dis[V1], C2)
       将上述不等式进行变量的替换有:
            dis[Vk] <= max(Ck, C(k-1), ..... , C2) = 最优路径
       已知权值路径为最短路所以:dis[Vk] >= 最优路径
       所以dis[Vk] = 最优
得证。  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3.3 路径长度为权值最小值

同理不在证明

4. Bellman-Ford算法

4.1 前置知识:

假设一个图G含有n个点,那么最短路所含有的边数最多为n - 1。

4.2 Bellman-Ford算法的思路:

因为最短路不可能包含重复的点,所以最短路中的每条边都连接两个不同的点,所以最多包含n - 1条边,不然的话最短路就有大于n个的点了(除了一种情况那就是负环,存在负环的图的最短路是无限的,所以可以用Bellman-Ford算法在循环第n次dis数组还是发生了更新的时候说明存在负环)。

如果能在第i次循环找到包含i条边的最短路,那么只需要n - 1次循环就能找到所有的最短路(因为最短路所含有的路径数最长为n - 1)

4.3 Bellman-Ford算法流程:

// 定义边结构体
struct edge
{
    int a;
    int b;
    int c;
};

// 初始化dis数组
memset(dis, 0x3f, sizeof dis);
dis[st] = 0;
// 不断松弛
// n为图的点数
// m为图的边数
for (int i = 0; i < n - 1; ++ i)
{
    for (int j = 0; j < m; ++ j)
    {
        // a -> b
        // 放松操作
        int a = edges[j].a;
        int b = edges[j].b;
        int c = edges[j].c;
        if (dis[b] > dis[a] + c)
            dis[b] = dis[a] + c;    
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

4.4 证明:

可以利用数学归纳法证明上述代码能在第i次循环找到包含i条边的最短路。

  1. 第一次循环能正确求出包含一条路径的最短路

  2. 假设第k次能求出所有包含k条路径的最短路

  3. 设包含k+1条路径的最短路V1->V2->V3-> … -> V(k+1) 那么V1->V2->V3-> … Vk 一定是最短路,并且已经在第k次循环中被求出来了,所以只要在利用Vk -> V(k+1) 这条边放松一下,这个包含k+1条路径的最短路就求得了。

4.5 利用队列优化Bellman-Ford(国内叫SPFA)

在上述流程中可以发现大部分松弛是无意义的,比如在第一次循环中松弛能更新的只有和源点st相邻的点。更加一般的:在某一次循环中,只有dis[u]被更新过,那么以u为起点的边才可能进行有效松弛(如果上一次循环中没有更新dis[u],那么发现对以u为起点的边的放松操作在上一次循环和本次循环中是完全重复的),所以可以将更新的点u加入队列,不断更新。

code:

#include <iostream>
#include <vector>
#include <queue>
#include <cstring>

using namespace std;

const int N = 2510, INF = 0x3f3f3f3f;

struct edge
{
    int to;
    int val;
};

vector<edge> g[N]; // 邻接表
int dis[N];        // 出发点到各个点的距离
int n, m, st, ed;  // 自左向右以此为:点个数,边个数,出发点,结束点

void spfa()
{
    // 记录每个点是否在队列中,如果在需要加入点时发现队列中存在这个点那么无需加入。
    bool vis[N] = {0};
    queue<int> q;
    q.push(st);
    vis[st] = true;

    while(q.size())
    {
        int u = q.front();
        q.pop();
        vis[u] = false;

        int sz = g[u].size();
        for (int i = 0; i < sz; ++ i)
        {
            edge temp = g[u][i];
            if (dis[temp.to] > dis[u] + temp.val)
            {
                dis[temp.to] = dis[u] + temp.val;
                if (!vis[temp.to])
                    q.push(temp.to);
            }
        }
    }
}

int main(void)
{
    cin >> n >> m >> st >> ed;
    for (int i = 0; i < m; ++ i)
    {
        int a, b, c;
        cin >> a >> b >> c; //边起点,边终点,边长度
        g[a].push_back({b, c}); // 有向图只加入一个
        g[b].push_back({a, c}); // 无向图加入两个
    }

    memset(dis, INF, sizeof dis);
    dis[st] = 0;

    // 队列优化的弗洛伊德算法:可以应对重边自环负边但无法应对负环
    spfa();
    // 应对出现负边的情况
    if (dis[ed] <= INF/2) cout << dis[ed] << endl;
    // 无法到达
    else cout << -1 << endl;

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

5. Bellman-Ford算法(or SPFA)的应用

5.1 利用Bellman-Ford算法能够求出包含k条边的最短路。

在这里插入图片描述

code:

#include <iostream>
#include <cstring>

using namespace std;

const int N = 100010, INF = 0x3f3f3f3f;

struct edge
{
    int i, j, val;
};

edge e[N];
int n, m, k;
int dist1[550], dist2[550];

void bellman_ford()
{
    memset(dist1, INF, sizeof dist1);
    dist1[1] = 0;


    for (int i = 0; i < k; ++ i)
    {
        memcpy(dist2, dist1, sizeof(dist1));
        for (int j = 0; j < m; ++ j)
        {
            int a = e[j].i;
            int b = e[j].j;
            int val = e[j].val;

            if (dist1[b] > dist2[a] + val) dist1[b] = dist2[a] + val;
        }
    }
}

int main(void)
{
    cin >> n >> m >> k;
    for (int i = 0; i < m; ++ i)
    {
        int x, y, z;
        cin >> x >> y >> z;
        e[i] = {x, y, z};
    }

    bellman_ford();

    if (dist1[n] <= INF/2) cout << dist1[n];
    else cout << "impossible";
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

5.2 SPFA判断负环

在这里插入图片描述

code:

6. 补充

如何完整的解决第三部分中不同路径长度表示方法下的最短路问题?(拓展到不同形式的最短路问题)

  1. 证明松弛到无法松弛时,dis数组最优

  2. 证明算法能松弛到无法松弛

关于SPFA算法的好处: 第二点特别好证明,因为只要满足:在某一次循环中,只有dis[u]被更新过,那么以u为起点的边才可能进行有效松弛,SPFA算法结束时一定会达到无法松弛的状态。对比Bellman-Ford算法固定的循环n-1次,SPFA算法在队列为空时结束,此时一定无法靠松弛操作更新dis数组。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/423319
推荐阅读
相关标签
  

闽ICP备14008679号