图的最短路

本文详细介绍了图的最短路径问题,包括Floyd算法(用于多源最短路径)、Dijkstra算法(单源最短路径)和SPFA算法(改进的单源最短路径)。讨论了它们的特点、实现方式和适用场景,以及如何处理负权边和负权回路的情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

图的最短路

一、定义

从图中某一顶点 (源点) 出发,到达另一顶点 (终点) 的所有路径中 (路径可能不存在或者存在不止一条) , 各边权值之和最小的路径,称为最短路径;

最短路径问题是图论的经典问题;

最短路径问题分为两类,

  1. 求单个顶点和其他所有顶点的最短路径,称为单源最短路径问题 ;
  2. 求所有顶点相互之间的最短路径,称为多源最短路径问题 ;

对于以上两类最短路径问题,都有相应的有效算法予以解决。

二、Floyd 算法

1. Floyd 算法

Floyd 算法又称为插点法,是一种用于寻找给定的加权图中多源点之间最短路径的算法;

2. 特点

Floyd 算法用于计算多元最短路,可计算出现负边权时的最短路,实际应用中,很多题目不是求如何用 Floyd 求最短路,而是用 Floyd 的动态规划思想来解决类似 Floyd 的问题;

3. 实现

Floyd 算法基于动态规划方法,使用 dp 存放当前顶点之间的最短路径长度;

状态

dp[k][i][j]dp[k][i][j]dp[k][i][j] 为前 kkk 个节点中,顶点 iii 到顶点 jjj 的最短路径长度;

转移

floyd状态转移-1

在计算 (i,j)(i, j)(i,j) 之间的最短路径时,目前 (i,j)(i, j)(i,j) 之间的最短路径长度为 dp[i][j]dp[i][j]dp[i][j] (此时 iiijjj 不一定是直接连接) ,发现 iii 可以通过 kkk 结点到达 jjj

得到如下状态转移方程

dp[k][i][j]=min{dp[k−1][i][j],dp[k−1][i][k]+dp[k−1][k][j]} dp[k][i][j] = min \{dp[k - 1][i][j], dp[k - 1][i][k] + dp[k - 1][k][j]\} dp[k][i][j]=min{dp[k1][i][j],dp[k1][i][k]+dp[k1][k][j]}

由于第一位 kkk 只用了 kkkk−1k - 1k1 所以 dp 数组可以进行滚动数组优化,优化掉 kkk 维,则状态转移方程为

dp[i][j]=min{dp[i][j],dp[i][k]+dp[k][j]} dp[i][j] = min \{dp[i][j], dp[i][k] + dp[k][j]\} dp[i][j]=min{dp[i][j],dp[i][k]+dp[k][j]}

转移时,先枚举 kkk ,在枚举 i,ji, ji,j

松弛操作

更新两点的最短路径又称为松弛操作;

在进行松弛操作时,若 (i,k)(i, k)(i,k)(k,j)(k, j)(k,j) 之间无边,那么 dp[i][k]=dp[k][j]=INFdp[i][k] = dp[k][j] = INFdp[i][k]=dp[k][j]=INF 此时若 INFINFINF0x7ffffff0x7ffffff0x7ffffff ,那么 dp[i][k]+w[k][j]dp[i][k] + w[k][j]dp[i][k]+w[k][j] 就会溢出变成负数,此时松弛操作便会出错,准确来说, 0x7fffffff0x7fffffff0x7fffffff 不能满足无穷大加一个有穷的数依然是无穷大,而是变成了一个很小的负数;

因此,可以选用 0x3f3f3f3f0x3f3f3f3f0x3f3f3f3f0x3f3f3f3f0x3f3f3f3f0x3f3f3f3f 的十进制是 106110956710611095671061109567 ,是 10910^9109 级别的,与 0x7fffffff0x7fffffff0x7fffffff 一个数量级,而一般场合下的数据都是小于 10910^9109 的,所以它可以作为无穷大使用而不致出现数据大于无穷大的情形;

另一方面,由于一般的数据都不会大于 10910^9109 ,所以当我们把无穷大加上一个数据时,它并不会溢出,事实上 0x3f3f3f3f+0x3f3f3f3f=21222191340x3f3f3f3f+ 0x3f3f3f3f=21222191340x3f3f3f3f+0x3f3f3f3f=2122219134 ,这个数虽然非常大但却没有超过 intintint 的表示范围,因此 0x3f3f3f3f0x3f3f3f3f0x3f3f3f3f 还满足了无穷大加无穷大还是无穷大的要求;

4. 例子

示例-2

dp=[05∞7∞0423302∞∞10] dp=\left[ \begin{matrix} 0 & 5 & \infty & 7 \\ \infty & 0 & 4 & 2 \\ 3 & 3 & 0 & 2 \\ \infty & \infty & 1 & 0 \end{matrix} \right] dp=035034017220

考虑结点 1 作为中间点,

dp[2][3]=4<dp[2][1]+dp[1][3〕=∞+∞dp[2][3] = 4 < dp[2][1] + dp[1][3〕= \infty + \inftydp[2][3]=4<dp[2][1]+dp[1][3=+ ,不更新;

dp[2][4]=2<dp[2][1]+dp[1][4]=∞+7dp[2][4] = 2 < dp[2][1] + dp[1][4] = \infty + 7dp[2][4]=2<dp[2][1]+dp[1][4]=+7 ,不更新;

dp[3][2]=3<dp[3][1]+dp[1][2]=3+5dp[3][2] = 3 < dp[3][1] + dp[1][2] = 3 + 5dp[3][2]=3<dp[3][1]+dp[1][2]=3+5 ,不更新;

dp[3][4]=2<dp[3][1]+dp[1][4]=3+∞dp[3][4] = 2 < dp[3][1] + dp[1][4] = 3 + \inftydp[3][4]=2<dp[3][1]+dp[1][4]=3+ ,不更新;

dp[4][2]=∞=dp[4][1]+dp[1][2]=∞+5dp[4][2] = \infty = dp[4][1] + dp[1][2] = \infty + 5dp[4][2]==dp[4][1]+dp[1][2]=+5 ,不更新;

dp[4][3]=1<dp[4][1]+dp[1][3]=∞+∞dp[4][3] = 1 < dp[4][1] + dp[1][3] = \infty + \inftydp[4][3]=1<dp[4][1]+dp[1][3]=+ ,不更新;

没有变化;

最终 dp[i][j]dp[i][j]dp[i][j] 存储的就是从 iiijjj 的最短路径长度。

5. 代码

for (int k = 1; k <= n; k++) {
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
        }
    }
}

6. 求最短路径方案

思路

Floyd 算法在进行松弛操作时,若松弛操作成功,只能够确定 iiijjj 是经过 kkk ,不能确定 iii 的下一个结点是谁,但可知 iiikkk 的最短路径中 iii 的下一个结点与 iiijjj 的最短路径中 iii 的下一个结点相同;

则使用 path[i][j]path[i][j]path[i][j] 记录 iiijjj 最短路径中 iii 的直接后继编号,若松弛成功,则使得 path[i][j] = path[i][k] ,即 iiikkk 的最短路径中 iii 的直接后继成为了 iiijjj 最短路径中 iii 的直接后继;

若要求字典序最小的一组方案,则在松弛操作时两方案相等时相等时,若当前 path[i][j] > path[i][k] ,则令 path[i][j] = path[i][k] ;

pathpathpath 的初始化为,对于任意一边 (x,y)(x, y)(x,y) ,有 path[x][y] = y

输出时,使用递归进行遍历,传入当前需输出的变量 xxx ,输出后,继续递归输出 path[x][t] ,出口即为 path[x][t] == 0 时;

代码如下,

void floyd() {
	for (int k = 1; k <= n; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                if (dp[i][j] > dp[i][k] + dp[k][j]) {
                    dp[i][j] = dp[i][k] + dp[k][j];
                    path[i][j] = path[i][k];
                } else if (dp[i][j] == dp[i][k] + dp[k][j] && path[i][j] > path[i][k]) { // 字典序最小
                    path[i][j] = path[i][k];
                }   
            }
        }
    }
}
void print(int x) {
	if (path[x][t] == 0) return;
	printf("%d ", x);
    print(path[x][t]);
}

7. 变形

如果是一个没有边权的图,把相连的两点间的距离设为 dp[i][j]=true ,不相连的两点设为 dp[i]][j]=false ,用 Floyd 算法的变形;

void floyd() {
    for (int k = 1; k <= n; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                dp[i][j] |= (dp[i][k] & dp[k][j]);
            }
        }
    }
    return;
}

用这个办法可以判断一张图中的两点是否相连。

三、Dijkstra算法

1. Dijkstra 算法

Dijkstra 算法是单源最路径算法,即计算起点只有一个的情况到其他点的最短路径,其无法处理存在负边权的情况;

Dijkstra 算法基于一种贪心的思想,基本思想是设 G=(V,E)G = (V, E)G=(V,E) 是一个带权有向图,把图中顶点集合 VVV 分成两组:

  1. 己求出最短路径 ddd 的顶点集合,用 SSS 表示,初始时 SSS 中只有一个源点;
  2. 其余未确定最短路径的顶点集合,用 UUU 表示;

2. 过程

  1. 初始时, SSS 只包含源点 VVVVVV 到自己的距离为 0 ;

    UUU 包含除 VVV 外的其他顶点,源点到 UUU 中顶点 iii 的距离为边上的权,若 VVViii 有边连接,则距离就是边权,否则距离为 ∞\infty

  2. UUU 中选取一个顶点 uuu ,使得顶点 VVV 到顶点 uuu 的距离最小,然后把顶点 uuu 加入 SSS 中;

  3. 以顶点 uuu 为新考虑的中间点,对顶点 VVVuuu 中各顶点进行松弛操作;

  4. 重复步骤 2 和 3 直到 SSS 包含所有的顶点;

3. 例子

以下图为例,以结点 1 作为源点,计算结点1到其他各结点的最短路径;

disdisdis 数组存放源点1到结点 iii 的距离;

truetruetrue 表示已确定的最短路径, falsefalsefalse 表示未确定的最短路径;

示例-2

第1轮松弛操作

Dij-eg-1-3

dis

05∞\infty7
truefalsefalsefalse

UUU 集合中选择与 vvv (即结点1)距离最短的点结点2,将结点2加入到集合 SSS 中,确定了结点1到结点2的最短路长度为5;

并以结点2作为中间点对源点 vvvUUU 集合中的所有结点进行松弛操作;

dis[3]=∞<dis[2]+w[2][3]=5+4=9dis[3] = \infty < dis[2] + w[2][3] = 5 + 4 = 9dis[3]=<dis[2]+w[2][3]=5+4=9,更新 dis[3]=9dis[3] = 9dis[3]=9

dis[4]=7=dis[2]+w[2][4]=5+2=7dis[4] = 7 = dis[2] + w[2][4] = 5 + 2 = 7dis[4]=7=dis[2]+w[2][4]=5+2=7,不更新;

第2轮松弛操作

dij-eg-2-4

dis

0597
truetruefalsefalse

UUU 集合中选择与 vvv (即结点1)距离最短的点结点4,将结点4加入到集合 SSS 中,确定了结点1到结4点的最短路长度为7;

并以结点4作为中间点对源点 vvvUUU 集合中的所有结点进行松弛操作:

dis[3]=∞<dis[4]+w[4][3]=7+1=8dis[3] = \infty < dis[4] + w[4][3] = 7 + 1 = 8dis[3]=<dis[4]+w[4][3]=7+1=8,更新 dis[3]=8dis[3] = 8dis[3]=8

dis[4]=7=dis[2]+w[2][4]=5+2=7dis[4] = 7 = dis[2] + w[2][4] = 5 + 2 = 7dis[4]=7=dis[2]+w[2][4]=5+2=7,不更新;

第3轮松弛操作

dij-eg-3-5

dis

0587
truetruefalsetrue

UUU 集合中选择与 vvv (即结点1)距离最短的点结点3,将结点3加入到集合 SSS 中,确定了结点1到结3点的最短路长度为8;

UUU 集合为空, DijkstraDijkstraDijkstra 算法完成;

dij-eg-end-6

dis

0587
truetruetruetrue

4. 证明

Dijkstra 算法也是一种贪心算法。证明 Dijkstra 算法可以找到图中从源点 vvv 到其他所有顶点的最短路径长度;

数学归纳法证明

  1. 如果顶点 iiiSSS 中,则 dis[i]dis[i]dis[i] 给出了从源点到顶点 iii 的最短路径长度;
  2. 如果顶点 iii 不在 SSS 中,则 dis[i]dis[i]dis[i] 给出了从源点到顶点iii的最短特殊路径长度(不一定是最短路径),即该路径上的所有中间顶点都属于 SSS

初始时 SSS 中只有一个源点 vvv ,到其他顶点的路径就是从源点到相应顶点的边,显然 1, 2 是成立的;

假设向 SSS 中添加一个新顶点 uuu 之前,条件 1, 2 都成立;

条件1的归纳步骤;

对于每个在添加之前己经存在于 SSS 中的顶点 uuu ,不会有任何变化,条件1依然成立;

在顶点 uuu 加入到 SSS 之前,由假设可知 dis[u]dis[u]dis[u] 是源点到 uuu 的最短路径长度,还要验证从源点 vvv 到顶点 uuu 的最短路径没有经过任何不在 SSS 中的顶点;

假设存在这种情况,即沿着从源点 vvv 到顶点 uuu 的最短路径前进时,会遇到一个或多个不属于SSS 的顶点不含顶点 uuu 自己),设 xxx 是第一个这样的顶点,如下图所示;

dij-证明-归纳1-7

从源点 vvvxxx 是一条特殊路径,距离为 dis[x]dis[x]dis[x]

假设 xxxuuu 的距离为 w[x][u]w[x][u]w[x][u] ,由于边权非负,即 w[x][u]≥0w[x][u] \geq 0w[x][u]0 ,推出经 xxxuuu 的距离 dis[x]+w[x][u]≥dis[x]dis[x] + w[x][u] \geq dis[x]dis[x]+w[x][u]dis[x]

因为算法在选择 xxx 之前先选择了 uuu ,因此 dis[x]≥dis[u]dis[x] \geq dis[u]dis[x]dis[u] ,这样经过 xxxuuu 的距离dis[x]+w[x][u]≥dis[x]≥dis[u]dis[x] + w[x][u] \geq dis[x] \geq dis[u]dis[x]+w[x][u]dis[x]dis[u],至少是 dis[u]dis[u]dis[u]

现在验证了当 uuu 加到 SSS 中时,dis[u]dis[u]dis[u] 确定给出源点 vvv 到顶点 uuu 的最短路径长度,条件1是成立的;

条件2的归纳步骤;

考虑不属于 SSS 且不同于 uuu 的一个顶点 www ,当 uuu 加到 SSS 中时,从源点 vvvwww 的最特殊路径有两种可能;

  1. 不会变化;
  2. 现在经过顶点 uuu(也可能经过 SSS 中的其他顶点);

dij-证明-归纳2-8

对于第2种情况,设 xxx 是到达 www 之前经过 SSS 的最后一个,因此这条路径的长度就是 dist[x]+w[x][w]dist[x] + w[x][w]dist[x]+w[x][w] ;

对于任意 SSS 中的顶点 qqq (包括 uuu ),要计算 dist[w]dist[w]dist[w] 的值,就必须比较dist[w]dist[w]dist[w] 原先的值和 dist[q]+dist[q]+w[q][w]dist[q]+dist[q] + w[q][w]dist[q]+dist[q]+w[q][w] 的大小;

因为算法明确地进行这种比较以计算新的 dist[w]dist[w]dist[w] 值,所以往 SSS 中加入新顶点 uuu 时,dist[w]dist[w]dist[w] 为源点 vvv 到顶点 www 的最短特殊路径的长度,因此条件2也是成立的;

5. 代码

void dijkstra(int s) {
    for (int i = 1; i <= n; i++) {
        g[i][i] = 0, dis[i] = g[s][i];
    }
    vis[s] = true;//s为起点
    for (int i = 1; i < n; i++) {
        int minn = 2147483647, tot = -1;
        for (int j = 1; j <= n; j++) { // 在没有确认最短路的结点集合中找一个顶点tot,使得dis[tot]最小
            if (vis[j] == false && dis[j] < minn) {
                minn = dis[j], tot = j;
            }
        }
        vis[tot] = true; // tot标记为已确定的最短路径
        for (int j = 1; j <= n; j++) { // 枚举与tot相连的每个未确定的最短路的顶点
            if (vis[j] == false && dis[tot] + g[tot][j] < dis[j]) {
                dis[j] = dis[tot] + g[tot][j]; // 更新最短路径
            }
        }
    }
    return;
}

6. 优化

思路

计算 UUU 集合距离起点最小值时,可使用优先对列维护;

代码
void dijkstra(int s) {
    memset(dis, 0x3f, sizeof(dis));
    dis[s] = 0;
    priority_queue < edge > q; // 存储未确定最短路的结点集合
    q.push(edge({s, 0}));
    while (!q.empty()) {
        int t = q.top().to; // 取出距离源点最近的结点编号
        q.pop();
        if (vis[t]) continue; // 某一个结点可能在松弛操作时多次放入优先队列
        vis[t] = true;
        for (int i = 0; i < g[t].size(); i++) { // 遍历tot的直接后继且未确定最短路的特点,进行松弛操作
            int v = g[t][i].to, tot =  g[t][i].tot;
            if (dis[v] > dis[t] + tot) { // 如果某结点已经确定最短路,则不会被更新
                dis[v] = dis[t] + tot;
                q.push(edge({v, dis[v]}));
            }
        }
    }
    return;
}

7. 输出方案

则在每一次松弛操作成功时,记录可使此节点松弛操作成功的节点,最后递归输出;

void dijkstra(int s) {
    memset(dis, 0x3f, sizeof(dis));
    dis[s] = 0;
    priority_queue < edge > q; // 存储未确定最短路的结点集合
    q.push(edge({s, 0}));
    while (!q.empty()) {
        int t = q.top().to; // 取出距离源点最近的结点编号
        q.pop();
        if (vis[t]) continue; // 某一个结点可能在松弛操作时多次放入优先队列
        vis[t] = true;
        for (int i = 0; i < g[t].size(); i++) { // 遍历tot的直接后继且未确定最短路的特点,进行松弛操作
            int v = g[t][i].to, tot = g[t][i].tot;
            if (dis[v] > dis[t] + tot) { // 如果某结点已经确定最短路,则不会被更新
                dis[v] = dis[t] + tot;
                pre[v] = t;
                q.push(edge({v, dis[v]}));
            } else if (dis[v] == dis[t] + tot && t < pre[v]) { // 字典序最小
                pre[v] = t;
            }
        }
    }
    return;
}

8. 次短路

问题

对于一张有向图,求 sssttt 的严格次短路;

分析

则维护 Dijkstra 的优先队列时,分别维护最短与次短路,当最短路成功进行松弛操作时,次短路的值即为原最短路的值,当最短路松弛操作未成功时但次短路松弛操作成功时,则只更新次短路即可;

代码
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
#include <algorithm>
#define MAXN 5005
using namespace std;
int n, m, dis[MAXN][5];
bool vis[MAXN][5];
struct edge {
    int to, tot;
};
vector <edge> g[MAXN];
struct node {
    int to, tot, p;
    bool operator < (const node a) const {
        return tot > a.tot;
    }
};
void dijkstra(int s) {
    memset(dis, 0x3f, sizeof(dis));
    dis[s][1] = 0;
    priority_queue <node> q;
    q.push(node({s, 0, 1}));
    while (!q.empty()) {
        node t = q.top();
        q.pop();
        if (vis[t.to][t.p]) continue;
        vis[t.to][t.p] = true;
        for (int i = 0; i < g[t.to].size(); i++) {
            int v = g[t.to][i].to, tot = g[t.to][i].tot;
            if (dis[v][1] > dis[t.to][t.p] + tot) {
                dis[v][2] = dis[v][1];
                dis[v][1] = dis[t.to][t.p] + tot;
                q.push(node({v, dis[v][1], 1}));
                q.push(node({v, dis[v][2], 2}));
            } else if (dis[v][1] < dis[t.to][t.p] + tot && dis[v][2] > dis[t.to][t.p] + tot) {
                dis[v][2] = dis[t.to][t.p] + tot;
                q.push(node({v, dis[v][2], 2}));
            }
        }
    }
    return;
}
int main() {
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        scanf("%d %d %d", &x, &y, &z);
        g[x].push_back(edge({y, z}));
        g[y].push_back(edge({x, z}));
    }
    dijkstra(1);
    printf("%d\n", dis[n][2]);
    return 0;
}

四、Bellman-Ford 算法

1. Bellman-Ford 算法

Bellman-Ford 算法适用于计算单源最短路径,其最大特点是可以处理存在负边权的情况,但无法处理存在负权回路的情况;

时间复杂度,O(V∗E)O(V*E)O(VE) ,其中, VVV 是顶点数,EEE 是边数;

2. 过程

对从源点到达每个结点的最短路径,每一轮都使用图中所有的边对其进行松弛操作,一共要进行 V−1V-1V1 轮松弛操作,其中 VVV 为顶点数;

使用 dis[i]dis[i]dis[i] 表示源点到结点 iii 的最短路径, 进行第 kkk 轮松弛操作后的距离计作 disk[i]dis_k[i]disk[i]

在进行第 kkk 轮松弛操作前,源点 vvv 到达结点 uuu 的距离为 disk−1[u]dis_{k - 1}[u]disk1[u] .在进行第 kkk 轮松弛操作时,uuu 的所有直接前驱与 uuu 连接的边才有可能对 vvvuuu 的最短路径产生影响;

如下图所示;

则第 kkk 轮松弛操作后,disk[u]dis_k[u]disk[u] 应为四条路径中的最小值,即,

BF-过程示例-9

disk[u]=min⁡{disk−1[u]disk−1[i]+w[i][u]disk−1[j]+w[j][u]disk−1[k]+w[k][u] dis_k[u] = \min \begin{cases} dis_{k - 1}[u] \\ dis_{k - 1}[i] + w[i][u] \\ dis_{k - 1}[j] + w[j][u] \\ dis_{k - 1}[k] + w[k][u] \\ \end{cases} disk[u]=mindisk1[u]disk1[i]+w[i][u]disk1[j]+w[j][u]disk1[k]+w[k][u]

即在每一轮松弛操作时,对每一条边所到达的结点 iii ( iiiuuu 的直接前驱)都要进行松弛操作;

disk[u]=min⁡{disk−1[u],min⁡1≤i≤n,i≠u{disk−1[i]+w[i][u]}} dis_k[u] = \min \{ dis_{k - 1}[u], \min_{1 \leq i \leq n, i \neq u}\{dis_{k - 1}[i] + w[i][u]\}\} disk[u]=min{disk1[u],1in,i=umin{disk1[i]+w[i][u]}}

BF-过程-10

3. 例子

如下图,以结点1作为起点演示 Bellman-Ford 算法;

BF-eg-fir-11

dis数组初始值

0∞\infty∞\infty∞\infty∞\infty∞\infty∞\infty
第一轮松弛操作

考虑结点 2, 3, 4 的前驱结点;

BF-eg-1-12

dis1[2]=∞>dis0[1]+w[1][2]=0+4=4dis_1[2] = \infty > dis_0[1] + w[1][2] = 0 + 4 = 4dis1[2]=>dis0[1]+w[1][2]=0+4=4 , 更新 dis1[2]=4dis_1[2] = 4dis1[2]=4 ;
dis1[3]=∞>dis0[1]+w[1][3]=0+(−6)=−6dis_1[3] = \infty > dis_0[1] + w[1][3] = 0 + (-6) = -6dis1[3]=>dis0[1]+w[1][3]=0+(6)=6 , 更新 dis1[3]=−6dis_1[3] = -6dis1[3]=6 ;
dis1[4]=∞>dis0[1]+w[1][4]=0+6=6dis_1[4] = \infty > dis_0[1] + w[1][4] = 0 + 6 = 6dis1[4]=>dis0[1]+w[1][4]=0+6=6 , 更新 dis1[4]=6dis_1[4] = 6dis1[4]=6 ;

dis数组

04-66∞\infty∞\infty∞\infty

考虑结点 5 的前驱结点;

BF-eg-2-13

dis1[5]=∞>dis0[2]+w[2][5]=4+7=11dis_1[5] = \infty > dis_0[2] + w[2][5] = 4 + 7 = 11dis1[5]=>dis0[2]+w[2][5]=4+7=11 , 更新 dis1[5]=11dis_1[5] = 11dis1[5]=11 ;
dis1[5]=11>dis0[3]+w[3][5]=−6+6=0dis_1[5] = 11 > dis_0[3] + w[3][5] = -6 + 6 = 0dis1[5]=11>dis0[3]+w[3][5]=6+6=0 , 更新 dis1[5]=0dis_1[5] = 0dis1[5]=0 ;
dis1[5]=0<dis0[6]+w[6][5]=∞+1=∞dis_1[5] = 0 < dis_0[6] + w[6][5] = \infty + 1 = \inftydis1[5]=0<dis0[6]+w[6][5]=+1= , 不更新;

dis数组

04-660∞\infty∞\infty

考虑结点6的前驱结点;

BD-eg-3-14

dis1[6]=∞>dis0[3]+w[3][6]=−6+4=−2dis_1[6] = \infty > dis_0[3] + w[3][6] = -6 + 4 = -2dis1[6]=>dis0[3]+w[3][6]=6+4=2 , 更新dis1[6]=−2dis_1[6] = -2dis1[6]=2
dis1[6]=−2<dis0[4]+w[4][6]=6+5=11dis_1[6] = -2 < dis_0[4] + w[4][6] = 6 + 5 = 11dis1[6]=2<dis0[4]+w[4][6]=6+5=11 , 不更新 ;

dis数组

04-660-2∞\infty

考虑结点7的前驱结点;

BF-eg-4-15

dis1[7]=∞>dis0[5]+w[5][7]=0+6=6dis_1[7] = \infty > dis_0[5] + w[5][7] = 0 + 6 = 6dis1[7]=>dis0[5]+w[5][7]=0+6=6,更新dis1[7]=6dis_1[7] = 6dis1[7]=6;
dis1[7]=6>dis0[6]+w[6][7]=−2+(−8)=−10dis_1[7] = 6 > dis_0[6] + w[6][7] = -2 + (-8) = -10dis1[7]=6>dis0[6]+w[6][7]=2+(8)=10,更新dis1[7]=−10dis_1[7] = -10dis1[7]=10;

dis数组:

04-660-2-10

第一轮松弛操作结束

dis数组操作如下

disk[1]dis_k[1]disk[1]disk[2]dis_k[2]disk[2]disk[3]dis_k[3]disk[3]disk[4]dis_k[4]disk[4]disk[5]dis_k[5]disk[5]disk[6]dis_k[6]disk[6]disk[7]dis_k[7]disk[7]
第一轮04-660-2-10
第二轮04-66-1-2-10
第三轮04-66-1-2-10
第四轮04-66-1-2-10
第五轮04-66-1-2-10
第六轮04-66-1-2-10

4. 解释

通过例子发现,从第二轮松弛操作之后,后面的最短路径值都没有发生过改变, Bellman-Ford 算法可以在这里进行优化。若某一轮松弛操作没有任何值发生变化,则算法可以直接结束;

每次松弛操作实际上是对相邻结点的访问,第 kkk 轮松弛操作保证了所有经过 kkk 条边的最短路径最短;

由于图的最短路径最长不会经过超过 V−1V-1V1 条边,所以可知 Bellman-Ford 算法所得为最短路径。

5. 判断负环

在执行完 V−1V-1V1 轮松弛操作之后,若发现还能够成功松弛操作,则说明图中存在负环;否则不存在负回路;

图为负环,需进行2轮松弛操作;

BF-负环-16

第一轮松弛操作

dis1[2]=dis0[1]+w[1][2]=0+1=1dis_1[2] = dis_0[1] + w[1][2] = 0 + 1 = 1dis1[2]=dis0[1]+w[1][2]=0+1=1 ;
dis1[3]=dis0[2]+w[2][3]=1+(−4)=−3dis_1[3] = dis_0[2] + w[2][3] = 1 + (-4) = -3dis1[3]=dis0[2]+w[2][3]=1+(4)=3 ;
dis1[l]=dis0[3]+w[3][1]=−3+2=−1dis_1[l] = dis_0[3] + w[3][1] = -3 + 2 = -1dis1[l]=dis0[3]+w[3][1]=3+2=1 ;

第二轮松弛操作

dis2[2]=dis1[1]+w[1][2]=−1+1=0dis_2[2] = dis_1[1] + w[1][2] = -1 + 1 = 0dis2[2]=dis1[1]+w[1][2]=1+1=0 ;
dis2[3]=dis1[2]+w[2][3]=−3dis_2[3] = dis_1[2] + w[2][3] = -3dis2[3]=dis1[2]+w[2][3]=3 ;
dis2[1]=dis1[3]+w[3][1]=−3+(−4)=−7dis_2[1] = dis_1[3] + w[3][1] = -3 + (-4) = -7dis2[1]=dis1[3]+w[3][1]=3+(4)=7 ;

若第三轮松弛操作仍能成功,则说明存在负环;

如果存在从源点可达的负权值回路(负回路),则最短路径不存在,因为可以重复走这个回路,使得路径无穷小。

6. 代码

bool Bellman_Ford (int s) { // s为起点
    memset(dis, 0x3f, sizeof(dis));
    dis[s] = 0;
    for (int i = 1; i < n; i++) { // n个顶点
        for (int j = 1; j <= m; j++) { // m条边
            dis[g[j].to] = min(dis[g[j].to], dis[g[j].from] + g[j].tot);
        }
    }
    for (int j = 1; j <= m; j++) {
        if (dis[g[j].to] > dis[g[j].from] + g[j].tot) {
            return false; // 有负环
        }
    }
    return true; // 没有负环
}

五、SPFA 算法

1. SPFA 算法

SPFA 算法也是一个求单源最短路径的算法,全称是 Shortest Path Faster Algorithm(SPFA) ,是由西南交通大学段凡丁老师1994年发明的;

当给定的图存在负权边时,Dijkstra 算法不再适合,而 Bellman-Ford 算法的时间复杂度又过高,此时可以采用 SPFA 算法;

但 SPFA 算法仍然不适合含负权回路的图;

2. 过程

初始时将起点加入队列;

每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若某个相邻的点修改成功,则将其入队,直到队列为空时算法结束;

这个算法,简单的说就是队列优化的 Bellman-Ford,利用了每个点不会更新次数太多的特点发明的此算法;

SPFA 在形式上和广度优先搜索非常类似,不同的是广度优先搜索中一个点出了队列就不可能重新进入队列,但是 SPFA 中一个点可能在出队列之后再次被放入队列,也就是说一个点修改过其它的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其它的点,这样反复进行下去;

对于负环时,则负环上的节点会一直进行松弛操作,即一直进队,则判断当有节点 nnn 次进入队时,则有负环;

3. 代码

bool SPFA(int s) {
	memset(dis, 0x3f, sizeof(dis));
	dis[s] = 0;
	queue <int> q;
	q.push(s); // 将起点入队
	while (!q.empty()) {
		int t = q.front();
		q.pop();
		vis[t] = false; // 出队首元素,并且将元素标记为出队
		for (int i = 0; i < g[t].size(); i++) { // 遍历结点u的所有后继结点,进行松弛操作
			int v = g[t][i].to, tot = g[t][i].tot;
			if (dis[v] > dis[t] + tot) {
				dis[v] = dis[t] + tot;
				if (!vis[v]) { // 若之前以入队,则不需要重复入队
					vis[v] = true;
					tot1[v]++; // 入队次数 = n, 有负环
					if (tot1[v] == n) return false;
					q.push(v);
				}
			}
		}
	}
	return true;
}

4. 第K短路

题目

给定一张N个点(编号1,2…N),M条边的有向图,求从起点S到终点T的第K短路的长度,路径允许重复经过点或边;

分析

求K短路可用类似 SPFASPFASPFA 算法的方法,即当第K次搜索到终点时,就为第K短路;

由于题目给出了起点与终点,可考虑用A*算法优化;

估价函数,

根据估价函数的设计准则,当前点 xxxeee 点的估计距离应不大于 xxxeee 点的实际距离,则把估价函数定为从 xxxeee 的最短路长度;

则程序思路如下

  1. 输入时,正反向建两个图,用反向建的图处理结点 xxx 到终点 eee 点的最短路;

  2. 从起点开始A*搜索扩展状态,当第K次搜索到 eee 结点时,就得到路径长;

当起点为终点时,会把起点算作一次,所以次数+1;

代码
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
#include <algorithm>
#define MAXN 1005
#define INF 0x3f3f3f3f
using namespace std;
int n, m, s, e, k;
struct edge {
	int to, tot;
};
vector < edge > g1[MAXN], g2[MAXN];
int dis[MAXN];
bool vis[MAXN];
void SPFA (int s) {
    memset(dis, INF, sizeof(dis));
    dis[s] = 0;
    queue < int > q;
    q.push(s);
    while (!q.empty()) {
        int tot = q.front();
        q.pop();
        vis[tot] = false;
        for (int i = 0; i < g2[tot].size(); i++) {
            int v = g2[tot][i].to, z = g2[tot][i].tot;
            if (dis[v] > dis[tot] + z) {
                dis[v] = dis[tot] + z;
                if (!vis[v]) {
                    vis[v] = true;
                    q.push(v);
                }
            }
        }
    }
}
struct node {
	int to, z, tot;
	bool operator < (const node t) const {
		return t.tot < tot;
	}
};
int a_star(int s, int e, int k) {
	if (dis[s] == INF) return -1;
	if (s == e) k++;
	int tot = 0;
	priority_queue < node > q;
	node t;
	q.push( node ( { s, 0, 0 + vis[s] } ) );
	while (!q.empty()) {
		t = q.top();
		q.pop();
		if (t.to == e) tot++;
		if (tot == k) return t.z;
		for (int i = 0; i < g1[t.to].size(); i++) {
			q.push( node ( { g1[t.to][i].to, t.z + g1[t.to][i].tot, t.z + g1[t.to][i].tot + dis[g1[t.to][i].to] } ) );
		}
	}
	return -1;
}
int main() {
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= m; i++) {
		int a, b, l;
		scanf("%d %d %d", &a, &b, &l);
		g1[a].push_back( edge ( { b, l } ) );
		g2[b].push_back( edge ( { a, l } ) );
	}
	scanf("%d %d %d", &s, &e, &k);
	SPFA(e);
	printf("%d", a_star(s, e, k));
	return 0;
}

六、比较

算法用途时间复杂度特点
Dijkstra单源最短路径O(n2)O(n^2)O(n2)不适合负权及负权回路
SPFA单源最短路径O(e)O(e)O(e)不适合负权回路
Bellman-Ford单源最短路径O(ne)O(ne)O(ne)不适合负权回路
Floyd多源最短路径O(n3)O(n^3)O(n3)不适合负权回路
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值