单源最短路问题——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] = 最优
得证。     

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] = 最优
得证。  

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;    
    }
}

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;
}

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;
}

5.2 SPFA判断负环

在这里插入图片描述

code:

6. 补充

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

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

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

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值