图的最短路

图的最短路

一、定义

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

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

最短路径问题分为两类,

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

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

二、Floyd 算法

1. Floyd 算法

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

2. 特点

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

3. 实现

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

状态

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

转移

floyd状态转移-1

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

得到如下状态转移方程

d p [ k ] [ i ] [ j ] = m i n { d p [ k − 1 ] [ i ] [ j ] , d p [ k − 1 ] [ i ] [ k ] + d p [ 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]}

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

d p [ i ] [ j ] = m i n { d p [ i ] [ j ] , d p [ i ] [ k ] + d p [ 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]}

转移时,先枚举 k k k ,在枚举 i , j i, j i,j

松弛操作

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

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

因此,可以选用 0 x 3 f 3 f 3 f 3 f 0x3f3f3f3f 0x3f3f3f3f 0 x 3 f 3 f 3 f 3 f 0x3f3f3f3f 0x3f3f3f3f 的十进制是 1061109567 1061109567 1061109567 ,是 1 0 9 10^9 109 级别的,与 0 x 7 f f f f f f f 0x7fffffff 0x7fffffff 一个数量级,而一般场合下的数据都是小于 1 0 9 10^9 109 的,所以它可以作为无穷大使用而不致出现数据大于无穷大的情形;

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

4. 例子

示例-2

d p = [ 0 5 ∞ 7 ∞ 0 4 2 3 3 0 2 ∞ ∞ 1 0 ] 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 作为中间点,

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

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

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

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

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

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

没有变化;

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

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 算法在进行松弛操作时,若松弛操作成功,只能够确定 i i i j j j 是经过 k k k ,不能确定 i i i 的下一个结点是谁,但可知 i i i k k k 的最短路径中 i i i 的下一个结点与 i i i j j j 的最短路径中 i i i 的下一个结点相同;

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

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

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

输出时,使用递归进行遍历,传入当前需输出的变量 x x x ,输出后,继续递归输出 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) 是一个带权有向图,把图中顶点集合 V V V 分成两组:

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

2. 过程

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

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

  2. U U U 中选取一个顶点 u u u ,使得顶点 V V V 到顶点 u u u 的距离最小,然后把顶点 u u u 加入 S S S 中;

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

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

3. 例子

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

d i s dis dis 数组存放源点1到结点 i i i 的距离;

t r u e true true 表示已确定的最短路径, f a l s e false false 表示未确定的最短路径;

示例-2

第1轮松弛操作

Dij-eg-1-3

dis

05 ∞ \infty 7
truefalsefalsefalse

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

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

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

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

第2轮松弛操作

dij-eg-2-4

dis

0597
truetruefalsefalse

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

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

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

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

第3轮松弛操作

dij-eg-3-5

dis

0587
truetruefalsetrue

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

U U U 集合为空, D i j k s t r a Dijkstra Dijkstra 算法完成;

dij-eg-end-6

dis

0587
truetruetruetrue

4. 证明

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

数学归纳法证明

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

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

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

条件1的归纳步骤;

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

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

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

dij-证明-归纳1-7

从源点 v v v x x x 是一条特殊路径,距离为 d i s [ x ] dis[x] dis[x]

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

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

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

条件2的归纳步骤;

考虑不属于 S S S 且不同于 u u u 的一个顶点 w w w ,当 u u u 加到 S S S 中时,从源点 v v v w w w 的最特殊路径有两种可能;

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

dij-证明-归纳2-8

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

对于任意 S S S 中的顶点 q q q (包括 u u u ),要计算 d i s t [ w ] dist[w] dist[w] 的值,就必须比较 d i s t [ w ] dist[w] dist[w] 原先的值和 d i s t [ q ] + d i s t [ q ] + w [ q ] [ w ] dist[q]+dist[q] + w[q][w] dist[q]+dist[q]+w[q][w] 的大小;

因为算法明确地进行这种比较以计算新的 d i s t [ w ] dist[w] dist[w] 值,所以往 S S S 中加入新顶点 u u u 时, d i s t [ w ] dist[w] dist[w] 为源点 v v v 到顶点 w w w 的最短特殊路径的长度,因此条件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. 优化

思路

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

代码
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. 次短路

问题

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

分析

则维护 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) ,其中, V V V 是顶点数, E E E 是边数;

2. 过程

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

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

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

如下图所示;

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

BF-过程示例-9

d i s k [ u ] = min ⁡ { d i s k − 1 [ u ] d i s k − 1 [ i ] + w [ i ] [ u ] d i s k − 1 [ j ] + w [ j ] [ u ] d i s k − 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]=min disk1[u]disk1[i]+w[i][u]disk1[j]+w[j][u]disk1[k]+w[k][u]

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

d i s k [ u ] = min ⁡ { d i s k − 1 [ u ] , min ⁡ 1 ≤ i ≤ n , i ≠ u { d i s k − 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

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

dis数组

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

考虑结点 5 的前驱结点;

BF-eg-2-13

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

dis数组

04-660 ∞ \infty ∞ \infty

考虑结点6的前驱结点;

BD-eg-3-14

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

dis数组

04-660-2 ∞ \infty

考虑结点7的前驱结点;

BF-eg-4-15

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

dis数组:

04-660-2-10

第一轮松弛操作结束

dis数组操作如下

d i s k [ 1 ] dis_k[1] disk[1] d i s k [ 2 ] dis_k[2] disk[2] d i s k [ 3 ] dis_k[3] disk[3] d i s k [ 4 ] dis_k[4] disk[4] d i s k [ 5 ] dis_k[5] disk[5] d i s k [ 6 ] dis_k[6] disk[6] d i s k [ 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 算法可以在这里进行优化。若某一轮松弛操作没有任何值发生变化,则算法可以直接结束;

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

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

5. 判断负环

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

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

BF-负环-16

第一轮松弛操作

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

第二轮松弛操作

d i s 2 [ 2 ] = d i s 1 [ 1 ] + w [ 1 ] [ 2 ] = − 1 + 1 = 0 dis_2[2] = dis_1[1] + w[1][2] = -1 + 1 = 0 dis2[2]=dis1[1]+w[1][2]=1+1=0 ;
d i s 2 [ 3 ] = d i s 1 [ 2 ] + w [ 2 ] [ 3 ] = − 3 dis_2[3] = dis_1[2] + w[2][3] = -3 dis2[3]=dis1[2]+w[2][3]=3 ;
d i s 2 [ 1 ] = d i s 1 [ 3 ] + w [ 3 ] [ 1 ] = − 3 + ( − 4 ) = − 7 dis_2[1] = dis_1[3] + w[3][1] = -3 + (-4) = -7 dis2[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 中一个点可能在出队列之后再次被放入队列,也就是说一个点修改过其它的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其它的点,这样反复进行下去;

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

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短路可用类似 S P F A SPFA SPFA 算法的方法,即当第K次搜索到终点时,就为第K短路;

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

估价函数,

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

则程序思路如下

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

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

当起点为终点时,会把起点算作一次,所以次数+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 ( n 2 ) O(n^2) O(n2)不适合负权及负权回路
SPFA单源最短路径 O ( e ) O(e) O(e)不适合负权回路
Bellman-Ford单源最短路径 O ( n e ) O(ne) O(ne)不适合负权回路
Floyd多源最短路径 O ( n 3 ) O(n^3) O(n3)不适合负权回路
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值