最短路算法:Dijkstra算法、Bellman-Ford算法、spfa算法和Floyd算法

简介

单源最短路:求所有点到某源点的最短路径。
多源最短路:求所有点到某些/所有源点的最短路径。
最短路算法的分类:

单源最短路
    所有边权都是正数
        朴素的Dijkstra算法 O(v^2) 适合稠密图
        堆优化版的Dijkstra算法 O(elogv)适合稀疏图,set或手写堆:O(elogv),优先队列:O(eloge),因为差距在log内,所以可以忽略不计
    存在负权边
        Bellman-Ford O(ve) 可用于判断是否有负环:v次循环后,再进行一次循环仍有状态更新
        spfa 一般O(e),最坏O(ve)
多源最短路 
	 Floyd算法 O(v^3)

在这里插入图片描述稠密图用邻接矩阵存,稀疏图用邻接表存储。

稠密图: m 和 n² 一个数量级
稀疏图: m 和 n 一个数量级

1.Dijkstra算法

思路

Dijkstra算法采用的是一种贪心的策略,声明一个数组dist来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0 (dist[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dist[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s。
然后,从dist数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点,
然后,我们需要看看新加入的顶点是否可以到达其他顶点c并且看看通过该顶点到达其他点的路径长度是否比dist[c]小,如果是,那么就替换这些顶点在dist中的值。
然后,又从dist中找出最小值,重复上述动作,直到T中包含了图的所有顶点。

伪代码

集合S:当前已经确定最短距离的点
dist[1] = 0, dist[i] = 正无穷 (i不等于1)
for v: 1 ~ n
    找出不在S中的距离最近的点t(S中最小dist[t])
    把t放到S中
    用t更新其他点的距离(对t的所有边进行松弛操作)

1.1 朴素的Dijkstra算法

使用场景

适合于稠密图,时间复杂度O(n²),用邻接矩阵(或邻接表)来存储。

算法模板

class Solution {
public:
    int networkDelayTime(vector<vector<int>>& times, int n, int K) {
        const int INF = 0x3f3f3f3f;
        vector<vector<int>> g(n+1, vector<int>(n+1, INF));
        for (auto &v: times){//建立邻接矩阵
            g[v[0]][v[1]] = v[2];
        }
        vector<int> dist(n+1, INF); // 距离起始点的最短距离,节点1~n
        vector<bool> st(n+1, false); // 是否已经得到最优解
        dist[K] = 0; // 起始点
        for (int i = 0; i< n - 1; i++ ){
            int t = -1;
            for (int j = 1; j <=n; j++){ // 在还未确定最短路的点中,寻找到起始点距离最小的点 的点
                if (!st[j] && (t == -1 || dist[t] > dist[j])){ 
                    t = j;
                }
            }
            st[t] = true; // t号点的最短路已经确定
            for (int j = 1; j<=n; j++){ // 用t更新其他点的距离
                dist[j] = min(dist[j], dist[t] + g[t][j]); 
            }
        }
        int ans = *max_element(dist.begin()+1, dist.end());
        return ans == INF ? -1: ans;
    }
};

1.2 堆优化版的Dijkstra算法

使用场景

适合于稀疏图,用邻接表来存储数据,调用set或手写堆时间复杂度O(elogv),调用优先队列时间复杂度O(eloge)。

模板

// first表示另一个端点, second表示距离/权重
using PII = pair<int, int>;
// dist[i]表示点i到k的最短路径
vector<int> dist;
vector<vector<PII>> graph;  // 邻接表;first表示另一个端点, second表示距离/权重
// n个顶点,edges[0]、edges[1]、edges[2]分别表示点u、v、两点距离
void Dijkstra(int n, vector<vector<int>>& edges, int k) {
    const int inf = 0x3f3f3f3f;
    dist = vector<int>(n + 1, inf);
    dist[k] = 0;
    vector<bool> exist(n + 1, 0);  // 最短距离是否已确定
    graph = vector<vector<PII>>(n + 1);
    for (const auto& vec : edges) {
        graph[vec[0]].push_back({ vec[1], vec[2] });
        graph[vec[1]].push_back({ vec[0], vec[2] });
    }
    priority_queue<PII, vector<PII>, greater<>> pq;
    pq.push({ 0, k });
    while (pq.size()) {
        auto [dis, v] = pq.top();
        pq.pop();
        if (exist[v])
            continue;  // v已经作为顶点,松驰过临边
        exist[v] = true;
        for (auto [u, d] : graph[v]) {
            if (d + dis < dist[u]) {
                dist[u] = d + dis;
                pq.push({ dist[u], u });
            }
        }
    }
}

【注】因为每个边都可能添加到堆中,所以严格来说是O(eloge)时间复杂度。如果想要严格O(elogn),可以使用红黑树set实现,通过删除set中旧节点,来保证不会有重复节点。

2.Bellman-Ford算法

2.1 算法思想

在这里插入图片描述
【注】上述公式适用于无边数限制,如果有边数K限制,则循环K次,上述松弛操作修改为:dis[v]=min(dist[v],pre[u]+w);//pre为上一次循环的结果

【证明】
设dp(k, v)表示最多经过k个节点中转由src到v的最低花费;
则dp(k, v) = min{dp(k-1, v), dp(k-1, u) + cost(u, v), dp(k-1,w)+ cost(u,w),…}
(u,w,…为v的所有邻接节点)

2.2 应用

在这里插入图片描述

2.3 代码

class Solution {
public:
    int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
        const int INF=0x3f3f3f3f;
        vector<int> dis(n,INF);
        dis[src]=0;
        for(int i=0;i<=K;i++){
            vector<int> pre=dis;
            for(auto flight:flights){
                dis[flight[1]]=min(dis[flight[1]],pre[flight[0]]+flight[2]);
            }
        }
        return dis[dst]==INF?-1:dis[dst];
    }
};

时间复杂度O(ve)。

3.spfa算法

在这里插入图片描述

3.1 算法思路

在这里插入图片描述

3.2 算法步骤

在这里插入图片描述

3.3 使用场景

在这里插入图片描述

3.4 spfa算法求负环

在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;

typedef pair<int, int> PII;
unordered_map<int, vector<PII>> edges; // 邻接表
int n, m; // n个点,m条边
const int N = 2010;
int dist[N]; // 到起始点的最小距离
bool st[N];  // 在队列中是否存在
int cnt[N]; // 记录最短路的边数

bool spfa(){
    queue<int> q;
    for (int i = 1; i <=n; i++){ // 所有点入队列;负环可能存在在所有点出发的最短路上
        q.push(i);
        st[i] = true;
    }

    while (!q.empty()){
        int u = q.front();
        q.pop();
        st[u] = false; // 不在队列

        for (auto &e: edges[u]){
            int v = e.first, w = e.second;
            if (dist[v] > dist[u] + w){
                dist[v] = dist[u] + w; // 更新最短路 权值
                cnt[v] = cnt[u] + 1; // 更新经过的边数
                // 存在负环;边数>=n,经过的点>=n+1;根据抽屉原理得,最短路存在负环
                if (cnt[v] >= n) return true; 
                if (!st[v]){
                    q.push(v);
                    st[v] = true;
                }
            }
        }
    }
    return false;
}

int main(){
    cin >> n >> m;
    while (m--){ // 构造图
        int u, v, w; 
        cin >> u>> v>> w;
        edges[u].push_back({v, w});
    }
    if (spfa()) puts("Yes");
    else puts("No");
    return 0;
}

4.Floyd算法

4.1 代码

求多源汇最短路径

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]);

4.2 算法原理

floyd算法是基于动态规划的
在这里插入图片描述



5. 例题

在这里插入图片描述
【方法一】朴素Dijkstra

class Solution {
public:
    int networkDelayTime(vector<vector<int>>& times, int n, int K) {
        const int INF = 0x3f3f3f3f;
        vector<vector<int>> g(n+1, vector<int>(n+1, INF));
        for (auto &v: times){//建立邻接矩阵
            g[v[0]][v[1]] = v[2];
        }
        vector<int> dist(n+1, INF); // 距离起始点的最短距离
        vector<bool> st(n+1, false); // 是否已经得到最优解
        dist[K] = 0; // 起始点
        for (int i = 0; i< n - 1; i++ ){
            int t = -1;
            for (int j = 1; j <=n; j++){ // 在还未确定最短路的点中,寻找到起始点距离最小的点 的点
                if (!st[j] && (t == -1 || dist[t] > dist[j])){ 
                    t = j;
                }
            }
            st[t] = true; // t号点的最短路已经确定
            for (int j = 1; j<=n; j++){ // 用t更新其他点的距离
                dist[j] = min(dist[j], dist[t] + g[t][j]); 
            }
        }
        int ans = *max_element(dist.begin()+1, dist.end());
        return ans == INF ? -1: ans;
    }
};

【方法二】堆优化的Dijkstra

class Solution {
public:
    int networkDelayTime(vector<vector<int>>& times, int N, int K) {
        const int INF = 0x3f3f3f3f;
        typedef pair<int, int> PII; // first:距离; second: 几号点
        vector<int> dist(N+1, INF); // 距离起始点的最短距离
        vector<vector<PII>> graph(n+1); // 邻接表;u->v,权重w
        set<PII,greater<PII>> iset;
        for (auto &t: times){ // 初始化邻接表
            graph[t[0]].push_back({t[2],t[1]});
        }
        iset.insert({0,K});
        dist[K] = 0;
        while(iset.size()){
            PII t= *iset.begin();
            iset.erase(iset.begin());
            int ver=t.second, distance= t.first;
            for (auto &p: graph[ver]){
                if (dist[p.second] > distance + p.first){ // 用t去更新其他点到起始点的最短距离
                    iset.erase({dist[p.second],p.second});
                    dist[p.second] = distance + p.first;
                    iset.insert({dist[p.second], p.second});
                }
            }
        }
        int ans = *max_element(dist.begin()+1, dist.end());
        return ans == INF ? -1: ans;
    }
};

【方法三】Bellman-Ford算法

class Solution {
public:
    int networkDelayTime(vector<vector<int>>& times, int N, int K) {
        const int INF=0x3f3f3f3f;
        vector<int> distoK(N+1,INF);
        distoK[K]=0;
        for(int i=0;i<N;i++){
        	vector<int> pre=distoK;
            for(vector<int> &time:times){
                distoK[time[1]]=min(distoK[time[1]],pre[time[0]]+time[2]);
            }
        }
        int res=*max_element(distoK.begin()+1,distoK.end());
        return res==INF?-1:res;
    }
};

【方法四】SPFA

class Solution {
public:
    int networkDelayTime(vector<vector<int>>& times, int N, int K) {
        const int INF = 0x3f3f3f3f;
        typedef pair<int, int> PII; // first:距离; second: 几号点
        vector<int> dist(N+1, INF); // 距离起始点的最短距离
        dist[K]=0;//初始点,容易忘
        vector<vector<PII>> graph(N+1); // 邻接表;u->v,权重w
        for (auto &t: times){ // 初始化邻接表
            graph[t[0]].push_back({t[2],t[1]});
        }
        queue<int> modifyque;
        modifyque.push(K);
        vector<bool> st(N+1, false); // 是否在队列中,避免重复进队列
        st[K] = true; 
        while(!modifyque.empty()){
            int tmp=modifyque.front();
            modifyque.pop();
            st[tmp]=false;
            for(PII &p:graph[tmp]){
                int cost=p.first;
                int dest=p.second;
                if(dist[tmp]+cost<dist[dest]){
                    dist[dest]=dist[tmp]+cost;
                    if(!st[dest]){
                        modifyque.push(dest);
                        st[dest]=true;
                    }
                }
            }
        }
        int ans = *max_element(dist.begin()+1, dist.end());
        return ans == INF ? -1: ans;
    }
};

【方法五】floyd算法

class Solution {
public:
    int networkDelayTime(vector<vector<int>>& times, int N, int K) {
        const int INF = 0x3f3f3f3f;
        vector<vector<int>> d(N+1, vector<int>(N+1, INF));
        for (int i = 1; i<=N; i++) d[i][i] = 0;

        for (auto &t: times){
            d[t[0]][t[1]] = min(d[t[0]][t[1]], t[2]);
        }

        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 ans = 0;
        for (int i =1; i<=N; i++){
            ans = max(ans, d[K][i]);
        }
        return ans > INF/2 ? -1: ans;
    }
};

参考:
https://leetcode-cn.com/problems/network-delay-time/solution/dan-yuan-zui-duan-lu-po-su-de-dijkstra-dui-you-hua/

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值