你真的了解最短路算法了吗?一文带你检验!!

185 阅读14分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

常见的最短路问题有哪些?

源点: 起点 汇点: 终点

判断图是什么图:m是 n2n^2 级别的话就是稠密图,m是 nn 级别的就是稀疏图(n是点数,m是边数)

边权: 离散数学或数据结构中,图的每条边上带的一个数值,他代表的含义可以是长度等等,这个值就是边权。

1.单源最短路 (只有一个起点)

求从一个点到其他所有点的最短距离,最常见的一个问题:从1号点到n号点的最短路

(1) 所有边权都是正数 (其中n为点的数量,m为边的数量)

  1. 朴素 DijkstraDijkstra 算法 时间复杂度 O(n2)O(n^2) (如果是一个稠密图(存储用邻接矩阵),例如 mmn2n^2 是一个级别的就用朴素 DijkstraDijkstra 算法)
  2. 堆优化版的 DijkstraDijkstra 算法 O(mlogn)O(mlogn) (如果是一个稀疏图(存储用邻接表),例如 mmnn 是一个级别的就用堆优化版的 DijkstraDijkstra 算法)

(2)存在负权边(某些边的权重是负数)

  1. BellmanFordBellman-Ford 时间复杂度 O(mn)O(mn) (如果说让我们求不超过k条边的最短路,用 BellmanFordBellman-Ford 算法比较好)

  2. SPFASPFA 一般情况下时间复杂度:O(m)O(m) ,最坏的情况下是:O(mn)O(mn)

    SPFASPFA算法是 BellmanFordBellman-Ford 算法的优化。

2.多源汇最短路 (有很多个不同的起点)

FloydFloyd 算法 O(n3)O(n^3)

最短路算法常见的考察方法:

原理:

​ (1) DijkstraDijkstra 算法基于贪心

​ (2) FloydFloyd 算法基于动态规划

​ (3) BellmanFordBellman-Ford 算法基于离散数学的知识

考点:建图。

把原情景问题抽象成一个最短路问题,如何定义点和边,使这个问题变成最短路问题,然后套用最短路模板来做

朴素 DijkstraDijkstra 算法

主要思想:每次找到离源点最近的一个顶点,然后以该点为中转点进行扩展( 看其余点到源点的距离是否能通过当前中转点到源点的最短距离加上中转点到该点的距离来更新其到源点的最短距离 ),最终找到源点到其余所有点的最短路径

① 初始化距离

让dis[1] = 0;// 1号点到起始点的距离是0

dis[其它所有] = +∞ // 让其他点到起点的距离为 ++∞

② 是一个迭代的过程 (set{s}:元素为当前已经确定了最短路的点,放到集合s里面)

​ for(0~n)

​ 1.在迭代的过程中找到不在s中的最短距离的点,赋值给t

​ 2.把t放到集合s里面去 n次

​ 3.用t更新其它所有点的距离(看一下dis[t] + w(权重) 与 dis[x],如果dis[x] > dis[t] + w,就可以用dis[t] + w更新其它点的距离)这样子可以保证每个点到1号点的距离是最短路,因为每一次dis[t]是最短的。

​ 解释一下:dis[t] + w 与 dis[x] 比较

在这里插入图片描述

简单例题:AcWing 849.Dijkstra求最短路 I(朴素Dijkstra算法 时间复杂度 O(n2)O(n^2)

题目描述:给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1

输入格式 第一行包含整数n和m。 接下来 m 行每行包含三个整数 a,y,z,表示存在一条从点 到点y 的有向边,边长为2

输出格式 输出一个整数,表示 1 号点到 n 号点的最短距离 如果路径不存在,则输出 -1。 数据范围 1 < n < 500.1 < m < 105 图中涉及边长均不超过10000。

输入样例:

3 3

1 2 2

2 3 1

1 3 4

输出样例: 3

步骤:

  1. 初始化

    • 将1号点初始化为0,其他所有点都初始化为+∞
  2. 迭代n次

    • 每次迭代都要找到当前没有迭代过的点的最小值,用st[ ]标记
  3. 用已经标记的你更新其他点

    • 每次迭代后用标记了的点t,按照点t的出边,找到它的下一个点x,用t更新x,t到x的边权是w

      如果dis[t]+w<dist[x]; 那么就用dist[t] + w更新dist[x];

  4. 当迭代n次之后,如果dis[n] == 无穷,则n号点没有最短路径

图解样例步骤:

在这里插入图片描述

AC代码:

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
const int N = 510;
int n, m;
int g[N][N];// 存储稠密图,用邻接矩阵
int dist[N];// 表示起点到每一个点当前距离是多少
bool st[N];//用于在更新最短距离时 判断当前的点的最短距离是否确定 是否需要更新

int dijkstra() {
    memset(dist, inf, sizeof dist);     //初始化距离  0x3f代表无限大

    dist[1] = 0;  //第一个点到自身的距离为0
    
    //有n个点所以要进行n次 迭代
    for(int i = 0; i < n; i++) {
        int t = -1;//t存储当前访问的点
        //先找最小值,这里的j代表的是从1号点开始遍历所有点
        for (int j = 1; j <= n; j++) {
            //如果当前这个点没有确定最短路,如果是t == -1应该直接赋值
            //或者是dist[t] > dist[j],就是说当前这个t,不是最短的,t的这个点的距离大于j的这个距离,
            if(!st[j] && (t == -1 || dist[t] > dist[j])) {
                t = j;//将t更新为j
            }
        }
        //将t这个点加入到集合中
        st[t]=true;   
        //用t这个点更新一下其他点的最短路,用1→t的距离+t→j的距离,更新1→j的距离
        for(int j = 1; j <= n; j++) dist[j] = min(dist[j], dist[t] + g[t][j]);
    }
    //如果第n个点路径为无穷大即不存在最低路径
    if(dist[n] == inf) return -1;
    return dist[n];
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin >> n >> m;
    
    //初始化图,因为是求最短路径,所以每个点初始为无限大
    memset(g, inf, sizeof g);
    
    while (m--) {
        int x, y, z;
        cin >> x >> y >> z;
        g[x][y] = min(g[x][y], z);//如果发生重边的情况,则 保留 边权最短的一条边
        //g[x][y] 最终存储的就是,x→y 边权,那么如果是发生了重边的情况,就会存储下最短的一条边
    }
    
    int res = dijkstra();
    cout << res << endl;
    return 0;
}

堆优化 DijkstraDijkstra 算法

朴素版的Dijkstra算法

  1. 初始化

    • 将1号点初始化为0,其他所有点都初始化为+∞
  2. 迭代n次

    • 每次迭代都要找到当前没有迭代过的点的最小值,用st[ ]标记
  3. 用已经标记的你更新其他点

    • 每次迭代后用标记了的点t,按照点t的出边,找到它的下一个点x,用t更新x,t到x的边权是w

      如果dis[t]+w<dist[x]; 那么就用dist[t] + w更新dist[x];

  4. 当迭代n次之后,如果dis[n] == 无穷,则n号点没有最短路径

第2、3点可以优化

第2点优化:找到没有迭代过的点的最小值,用堆查找最小值就是 O(1)O(1) 的操作

第3点优化:用t更新其他点的距离,如果是朴素算法那么会执行 n2n^2 次,而堆中修改n个值的时间复杂度是: O(logn)O(logn) 那么最多修改m次,就是 O(mlog(n2)=O(2mlogn)=O(mlogn)O(mlog(n^2) = O(2mlogn) = O(mlogn)

所以:可以用堆存储每个点到起点的最短距离,就可以把Dijkstra算法优化成 O(mlogn)O(mlogn)

两种实现堆的方式 {手写堆:保证堆里面只有n个元素,支持修改堆里面的任一元素STL(优先队列):不支持修改堆里面的任一元素,冗余,每次修改都会往堆里面放一个元素\begin{cases}手写堆:保证堆里面只有n个元素,支持修改堆里面的任一元素\\ \\STL(优先队列):不支持修改堆里面的任一元素,冗余,每次修改都会往堆里面放一个元素 \end{cases}

850. 堆优化 Dijkstra 算法(例题)

题目描述:给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为非负值请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1

输入格式:

第一行包含整数n和m。 接下来 m 行每行包含三个整数 x,y,,表示存在一条从点 到点y 的有向边,边长为2。

输出格式: 输出一个整数,表示1号点到n 号点的最短距离 如果路径不存在,则输出-1

数据范围 1<n,m<1.5×1051<n,m < 1.5×10^5

图中涉及边长均不小于0,且不超过10000

输入样例:

3 3

1 2 2

2 3 1

1 3 4

输出样例:

3

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
typedef pair<int, int> PII;
const int N = 150010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];
typedef pair<int, int> PII;

void add(int a, int b, int c) {
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx++;
}

//求1号点到n号点的最短路,如果不存在返回-1,存在返回值
int dijkstra(){
    memset(dist, inf, sizeof dist);
    dist[1] = 0;
    
    //开一个优先队列来维护当前未在st中标记过且里源点最近的点
    priority_queue<PII, vector<PII>, greater<PII>> heap;//小根堆
    //放入起点
    heap.push({0, 1});
    
    while (heap.size()) {
        //----1.取出当前未在st中出现过且离源点最近的点
        auto t = heap.top();
        heap.pop();
        
        //t.first最短路的距离值   t.second是点的编号
        int distance = t.first, ver = t.second;
        
        //如果之前被更新过的话,说明这个点是冗余备份,直接跳过就可以了
        if (st[ver]) continue;
        //----2.先将这个点标记
        //否则就用当前点来更新其他所有的点
        st[ver] = true;
        
        //----3.用t更新其他点的距离
        for (int i = h[ver]; i != -1; i = ne[i]) {
            int j = e[i];//j存储当前节点编号
            if (dist[j] > distance + w[i]) {
                dist[j] = distance + w[i];//如果更新成功的话
                heap.push({dist[j], j});//就将这个点放入队列
            }
        }
    }
    
    if (dist[n] == inf) return -1;
    return dist[n];
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin >> n >> m;
    memset(h, -1, sizeof h);
    
    while (m--) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    
    cout << dijkstra() << endl;
    return 0;
}

BellmanFordBellman-Ford 算法 (处理有负权边的最短路问题)

步骤:

  1. 迭代n次
  2. 每一次循环所有边
  3. dist[b] = min(dist[b], dist[a] + w);(松弛操作) w:a→b的权值
    • 存储边的方式,开一个结构体struct{ int a, b, w;}edge[M];

BellmanFordBellman-Ford 循环完所有的边之后,一定满足 dist[b] ≤ dist[a] + w;

时间复杂度:O(nm)O(nm)

如果能求出最短路,这个图里面是没有负权回路的

图中有负权回路,不一定存在最短路

如果存在负权回路,例子如下图所示:

在这里插入图片描述

2→3→4→2,每转一圈,从1到5的边权之和就-1,要想求最短路就一直转圈2→3→4→2,那最短路的权值是-∞。

迭代k次之后,这个式子:dist[b]≤dist[a]+w 的意义:从1号点经过不超过k条边走到每个点的最短距离

那么如果在迭代n次之后,还在更新某些边的话,那么就说明,存在一条最短路径,这条最短路径有n条边。

如果有n条边,那么这个路径一定经过了n+1个点,根据抽屉原理,这个路有n个抽屉,所以一定存在环,因为一直在更新,所以这个环一定是负环,如果是正环的话,最短路径也只是存在一条,不会一直在更新。所以Bellman-Ford算法可以用来找负环,但是时间复杂度比较高所以不用它来找负环。一般找负环都是用SPFA来做。

题目描述:给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数请你求出从1号点到n号点的最多经过k条边的最短距离,如果无法从1号点走到n号点,输出impossible。 注意:图中可能存在负权回路

输入格式 第一行包含三个整数n,m,k。 接下来 m 行,每行包含三个整数 ,y,z,表示存在一条从点 到点y 的有向边,边长为2。

输出格式 输出一个整数,表示从1号点到n号点的最多经过条边的最短距离如果不存在满足条件的路径,则输出 impossible 。

数据范围 1<n,k<500,1<m<100001<n,k<500,1<m<10000 任意边长的绝对值不超过10000

输入样例:

3 3 1

1 2 1

2 3 1

1 3 3

输出样例:

3

//有边数限制的最短路,只能用Bellman-Ford算法来做
//边权可能为负数 ———— 不能用Dijkstra算法
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
const int N = 510;
const int M = 10010;
int n, m, k;
int dist[N];
int backup[N];//备份,backup存的是上一次迭代的结果,如若不然可能会发生串联
struct edge {
    int a, b, w;
}edges[M];

int bellmanFord() {
    //初始化所有点的值为正无穷
    memset(dist, inf, sizeof dist);
    dist[1] = 0;//初始化起点为0
    //最多经过k条边
    for (int i = 0; i < k; i++) {
        memcpy(backup, dist, sizeof dist);
        //枚举每条边
        for (int j = 0; j < m; j++) {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            dist[b] = min(dist[b], backup[a] + w);//更新
        }
    }
    return dist[n];
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin >> n >> m >> k;
    
    for (int i = 0; i < m; i++) {
        int a, b, w;
        cin >> a >> b >> w;
        edges[i] = {a, b, w};
    }
    
    int t = bellmanFord();
    if (t > inf / 2) cout << "impossible" << endl;
    else cout << t << endl;
    return 0;
}

待补充的两个问题:

  1. 为什么需要backup[a]备份?
    • back[] 数组是上一次迭代后 dist[] 数组的备份,由于是每个点同时向外出发,因此需要对 dist[] 数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点
  2. 为什么inf / 2?

SPFASPFA 算法

使用SPFA算法的条件:没有负环

如果是正权图用Dijkstra,如果是负权图用SPFA

SPFA算法是对Bellamn-Ford算法的优化

准确来说是对 dist[b] = min(dist[b], backup[a] + w); 这一步进行优化。

当backup[a]变小的时候,才会影响 dist[b] 的结果,所以开一个队列,先把起点存进去,之后如果有

队列存的是待更新的点
queue⬅1
while(queue不空) {
	Ⅰ.t⬅queue.front();//取出队首元素
	   q.pop();
	Ⅱ.更新t的所有出边 t→b(权值为w)
	   如果更新成功的话,就把b加入队列,b就是待更新的点,先判断b是否已经被更新过了,如果已经更新过了
}

851.spfa求最短路

题目描述:给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数请你求出1号点到n 号点的最短距离,如果无法从1号点走到n 号点,则输出 mpossible数据保证不存在负权回路。

输入格式: 第一行包含整数n和m。 接下来m行每行包含三个整数x,y,z,表示存在一条从点 到点y的有向边,边长为2。输出格式 输出一个整数,表示1号点到n号点的最短距离 如果路径不存在,则输出impossible 。

数据范围: 1<n,m<=1051< n,m < =105

图中涉及边长绝对值均不超过10000.

输入样例:

3 3

1 2 5

2 3 3

1 3 4

输出样例:

2

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
typedef pair<int, int> PII;
const int N = 100010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];//判断当前的点是否已经加入到队列当中了,如果节点已经加入队列了,就不需要重复的将该节点加入到队列中,但是依旧会更新到源点的距离,更新一下数值即可,没必要再加入到队列当中

void add(int a, int b, int c) {
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx++;
}

int spfa(){
    //初始化所有点的距离
    memset(dist, inf, sizeof dist);
    dist[1] = 0;//起点的距离为0
    
    //开个队列存储dist[a]减小的点
    queue<int> q;
    q.push(1);//把1号点放到队列里面
    st[1] = true;//st[]数组,判断当前这个点是否在队列中,重复的点没有意义
    
    while (q.size()) {
        int t = q.front();//取出队首元素
        q.pop();
        st[t] = false;
        
        //优化:
        //更新一下t的所有出边,所以用SPFA算法做的,会遍历到的节点都是与源点联通的,如果题目的n与源点不相通,就不会得到更新
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];//用j表示当前这个点的值
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                //如果队列里面已经有j点,就不用存到队列里面去了,值会更新
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return dist[n];
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin >> n >> m;
    memset(h, -1, sizeof h);
    
    while (m--) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    int ans = spfa();
    if (ans == inf) cout << "impossible" << endl;
    else cout << ans << endl;
    return 0;
}

852. spfa判断负环

题目描述:给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数请你判断图中是否存在负权回路。

输入格式 第一行包含整数n和m。 接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为2。

输出格式 如果图中存在负权回路,则输出 Yes,否则输出 No

数据范围 1<n<2000.1<n<2000. 1<m<10000,1< m<10000, 图中涉及边长绝对值均不超过10000。

输入样例:

3 3

1 2 -1

2 3 4

3 1 -4

输出样例: Yes

题解:在spfa求最短路的基础上开多一个数组cnt[]记录,当前路径经过的边数即可。从1到x经过了至少n条边,那么一定经过了n+1个点,图中只有n个点,由抽屉原理得,此路径中一定存在两个相同的点,由最短路径的原理可知:如果经过了两个相同的点,一定是存在负环。

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
typedef pair<int, int> PII;
const int N = 100010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N], cnt[N];//cnt[]表示从源点到当前点最短路径的边数
bool st[N];//判断当前的点是否已经加入到队列当中了,如果节点已经加入队列了,就不需要重复的将该节点加入到队列中,但是依旧会更新到源点的距离,更新一下数值即可,没必要再加入到队列当中

void add(int a, int b, int c) {
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx++;
}

bool spfa(){
    //求的不是距离的绝对值,只是判断图中是否存在负环
    // //初始化所有点的距离
    // memset(dist, inf, sizeof dist);
    // dist[1] = 0;//起点的距离为0
    
    //开个队列存储dist[a]减小的点
    queue<int> q;
    
    //不能只是从1开始,如果从1开始,只是查找以1为源点的最短路径中是否存在负环,如果图中存在以1为源点的路径到不了的负环,那就GG了。
    // q.push(1);//把1号点放到队列里面
    // st[1] = true;//st[]数组,判断当前这个点是否在队列中,重复的点没有意义
    
    //所以需要把所有点都放进去
    for (int i = 1; i <= n; i++) {
        q.push(i);
        st[i] = true;
    }
    
    while (q.size()) {
        int t = q.front();//取出队首元素
        q.pop();
        st[t] = false;
        
        //优化:
        //更新一下t的所有出边,所以用SPFA算法做的,会遍历到的节点都是与源点联通的,如果题目的n与源点不相通,就不会得到更新
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];//用j表示当前这个点的值
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                //如果边数≥n,就一定存在负环
                //抽屉原理,从1到x经过了至少n条边,那么一定经过了n+1个点,图中只有n个点,所以路径一定经过了两个相同的点
                //由最短路径的原理可知:如果经过了两个相同的点,一定是有负环。
                if (cnt[j] >= n) return true;
                //如果队列里面已经有j点,就不用存到队列里面去了,值会更新
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin >> n >> m;
    memset(h, -1, sizeof h);
    
    while (m--) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    
    if (spfa()) cout << "Yes" << endl;
    else cout << "No" << endl;
    return 0;
}

FloydFloyd 算法

解决:多源汇最短路

时间复杂度:O(n3)O(n^3)

原理:动态规划

d[k,i,j]d[k, i, j] :从 ii 这个点,只经过 11 ~ kk 这些中间点到达 jj 的最短距离

854. Floyd求最短路

题目描述:给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数再给定 k 个询问,每个询问包含两个整数 和 y,表示查询从点 到点 y 的最短距离,如果路径不存在,则输出impossible 数据保证图中不存在负权回路

输入格式 第一行包含三个整数n,m,k。 接下来 m 行,每行包含三个整数 ,y,,表示存在一条从点 到点y的有向边,边长为2。接下来m行,每行包含两个整数t,y,表示询问点t到点y 的最短距离

输出格式 共行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出impossible

数据范围 1<n<200,1 <n < 200, 1<k<n21<k<n^2 1<m<200001 < m <20000 图中涉及边长绝对值均不超过10000

输入样例: 3 3 2 1 2 1 2 3 2 1 3 1 2 1 1 3

输出样例: impossible 1

#include<bits/stdc++.h>
using namespace std;
const int N = 205, INF = 0x3f3f3f3f;
int n, m, q;

int d[N][N];//邻接矩阵:记录最短距离


void floyd(){
    for (int k = 1; k <= n; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
            }
        }
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin >> n >> m >> q;
    
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (i == j) d[i][j] = 0;//自环
            else d[i][j] = INF;
        }
    }
    
    while (m--) {
        int a, b, w;
        cin >> a >> b >> w;
        
        d[a][b] = min(d[a][b], w);
    }
    
    floyd();
    
    while (q--) {
        int a, b;
        cin >> a >> b;
        if (d[a][b] > INF / 2) cout << "impossible" << endl;
        else cout << d[a][b] << endl;
    }
    return 0;
}