三类最短路径算法简单介绍(Dijkstra、SPFA、Floyd)

三类最短路径算法简单介绍(Dijkstra、SPFA、Floyd)

1. D i j k s t r a Dijkstra Dijkstra算法

解决单源最短路径问题常用 Dijkstra 算法,用于计算一个顶点到其他所有顶点的最短路径。Dijkstra 算法的主要特点是以起点为中心,逐层向外扩展(这一点类似于 bfs,但是不同的是,bfs 每次扩展一个层,但是 Dijkstra 每次只会扩展一个点),每次都会取一个最近点继续扩展,直到取完所有点为止。

注意:Dijkstra 算法要求图中不能出现负权边。

①、 D i j k s t r a Dijkstra Dijkstra算法流程

我们定义带权图 G G G所有顶点的集合为 V V V,接着我们再定义已确定从源点出发的最短路径的顶点集合为 U U U,初始集合 U U U为空,记从源点 s s s出发到每个顶点 v v v的距离为 d i s t v dist_v distv,初始 d i s t s dist_s dists=0。接着执行以下操作:

  1. V − U V−U VU中找出一个距离源点最近的顶点 v v v,将 v v v加入集合 U U U

  2. 并用 d i s t v dist_v distv和顶点 $v $连出的边来更新和 v v v相邻的、不在集合 U U U中的顶点的 d i s t dist dist,这一步称为松弛操作。

  3. 重复步骤 1 和 2,直到 V = U V=U V=U或找不出一个从 s s s 出发有路径到达的顶点,算法结束。

如果最后 V ≠ U V \neq U V=U,说明有顶点无法从源点到达;否则每个 d i s t i dist_i disti表示从 s s s 出发到顶点$ i $的最短距离。

Dijkstra 算法的时间复杂度为 O ( V 2 ) \mathcal{O}(V^2) O(V2),其中 V V V 表示顶点的数量。

Dijkstra 是解决无负边权的图的单源最短路问题,经常使用邻接表存储。

不优化的时间复杂度是 O ( V 2 + E ) O(V^2 + E) O(V2+E)

#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e3 + 9;
const int M = 1e4 + 9;
const int inf = 0x3f3f3f3f;
struct edge {
    int v, w, fail;
    edge() {}
    edge(int _v, int _w, int _fail) {
        v = _v;
        w = _w;
        fail = _fail;
    }
} e[M << 1];
int head[N], len;
void init() {
    memset(head, -1, sizeof(head));
    len = 0;
}
void add(int u, int v, int w) {
    e[len] = edge(v, w, head[u]);
    head[u] = len++;
}
void add2(int u, int v, int w) {
    add(u, v, w);
    add(v, u, w);
}
int n, m;
int dis[N];
bool vis[N];
void dijkstra(int u) {
    memset(vis, false, sizeof(vis));
    memset(dis, 0x3f, sizeof(dis));
    dis[u] = 0;
    for (int i = 0; i < n; ++i) {
        int mi = inf;
        for (int j = 1; j <= n; ++j) {
            if (!vis[j] && dis[j] < mi) {
                mi = dis[u = j];
            }
        }
        if (mi == inf) {
            return;
        }
        vis[u] = true;
        for (int j = head[u]; ~j; j = e[j].fail) {
            int v = e[j].v;
            int w = e[j].w;
            if (!vis[v] && dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
            }
        }
    }
}
int main() {
    init();
    int u, v, w;
    cin >> n >> m;
    while (m--) {
        cin >> u >> v >> w;
        add2(u, v, w);
    }
    dijkstra(1);
    cout << dis[n] << endl;
    return 0;
}
②、基于小根堆优化的 D i j k s t r a Dijkstra Dijkstra算法

用一个set来维护点的集合,这样的时间复杂度就优化到了 O ( ( V + E ) log ⁡ V ) \mathcal{O}((V+E)\log V) O((V+E)logV),对于稀疏图的优化效果非常好

const int MAX_N = 10000;
const int MAX_M = 100000;
const int inf = 0x3f3f3f3f;
struct edge {
    int v, w, next;
} e[MAX_M];
int p[MAX_N], eid, n;
void mapinit() {
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v, int w) {  // 插入带权有向边
    e[eid].v = v;
    e[eid].w = w;
    e[eid].next = p[u];
    p[u] = eid++;
}
void insert2(int u, int v, int w) {  // 插入带权双向边
    insert(u, v, w);
    insert(v, u, w);
}
typedef pair<int, int> PII;
set<PII, less<PII> > min_heap;
int dist[MAX_N];  // 存储单源最短路的结果
bool vst[MAX_N];  // 标记每个顶点是否在集合 U 中
bool dijkstra(int s) {
    // 初始化 dist、小根堆和集合 U
    memset(vst, 0, sizeof(vst));
    memset(dist, 0x3f, sizeof(dist));
    min_heap.insert(make_pair(0, s));
    dist[s] = 0;
    for (int i = 0; i < n; ++i) {
        if (min_heap.size() == 0) {  // 如果小根堆中没有可用顶点,说明有顶点无法从源点到达,算法结束
            return false;
        }
        // 获取堆顶元素,并将堆顶元素从堆中删除
        set<PII, less<PII> >::iterator iter = min_heap.begin();
        int v = iter->second;
        min_heap.erase(*iter);
        vst[v] = true;
        // 进行和普通 dijkstra 算法类似的松弛操作
        for (int j = p[v]; j != -1; j = e[j].next) {
            int x = e[j].v;
            if (!vst[x] && dist[v] + e[j].w < dist[x]) {
                // 先将对应的 pair 从堆中删除,再将更新后的 pair 插入堆
                min_heap.erase(make_pair(dist[x], x));
                dist[x] = dist[v] + e[j].w;
                min_heap.insert(make_pair(dist[x], x));
            }
        }
    }
    return true;  // 存储单源最短路的结果
}
③、基于优先队列优化的 D i j k s t r a Dijkstra Dijkstra算法

我们在 n o d e node node节点里面记录对应的点的最短路,然后每次更新一个点的最短路后都把这个点压入到优先队列里面(不管之前有没有被压入到队列里面),这样就一定能够保证优先队列对的性质不会改变

这个代码的时间复杂度实际上会比用真正的堆要慢一点,因为有的点可能会入队多次,但是每一条边最多导致一次入队,所以这个算法的时间复杂度为 O ( E log ⁡ E ) \mathcal{O}(E\log E) O(ElogE)。其中$ E $为边的数量。

const int MAX_N = 10000;
const int MAX_M = 100000;
const int inf = 0x3f3f3f3f;
struct edge {
    int v, w, next;
} e[MAX_M];
int p[MAX_N], eid, n;
void mapinit() {
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v, int w) {  // 插入带权有向边
    e[eid].v = v;
    e[eid].w = w;
    e[eid].next = p[u];
    p[u] = eid++;
}
void insert2(int u, int v, int w) {  // 插入带权双向边
    insert(u, v, w);
    insert(v, u, w);
}
int dist[MAX_N];  // 存储单源最短路的结果
bool vst[MAX_N];  // 标记每个顶点是否在集合 U 中
struct node {
    int u;
  int dist;
    node(int _u, int _dist) : u(_u), dist(_dist) {}
    bool operator < (const node &x) const {
        return dist > x.dist;
    }
}; // 记录点的结构体
bool dijkstra(int s) {
    // 初始化 dist、小根堆和集合 U
    memset(vst, 0, sizeof(vst));
    memset(dist, 0x3f, sizeof(dist));
    priority_queue<node> min_heap;
    dist[s] = 0;
    min_heap.push(node(s, 0));
    while (!min_heap.empty())
        // 获取堆顶元素,并将堆顶元素从堆中删除
        int v = min_heap.top().u;
        min_heap.pop();
        if (vst[v]) {
            continue;
        }
        vst[v] = true;
        // 进行和普通 dijkstra 算法类似的松弛操作
        for (int j = p[v]; j != -1; j = e[j].next) {
            int x = e[j].v;
            if (!vst[x] && dist[v] + e[j].w < dist[x]) {
                dist[x] = dist[v] + e[j].w;
                min_heap.push(node(x, dist[x]));
            }
        }
    }
    return true;
}

2. S P F A SPFA SPFA算法

SPFA(Shortest Path Faster Algorithm)算法是单源最短路径的一种算法,通常被认为是 Bellman-ford 算法的队列优化,在代码形式上接近于宽度优先搜索 BFS,是一个在实践中非常高效的单源最短路算法。

①、 S P F A SPFA SPFA算法流程

S P F A SPFA SPFA 算法中,使用 d i d_i di表示从源点到顶点 i i i的最短路,额外用一个队列来保存即将进行拓展的顶点列表,并用 i n q i inq_i inqi来标识顶点 i i i是不是在队列中。

1.初始队列中仅包含源点,且源点 s s s d s = 0 d_s=0 ds=0

2.取出队列头顶点 u u u,扫描从顶点 u u u 出发的每条边,设每条边的另一端为 v v v,边 < u , v > <u,v> <u,v> 权值为 w w w,若 d u + w < d v d_u+w<d_v du+w<dv,则

  • d v d_v dv修改为 d u + w d_u+w du+w

  • v v v 不在队列中,则

  • 将 $v $入队

3.重复步骤 2 直到队列为空

最终$ d$ 数组就是从源点出发到每个顶点的最短路距离。如果一个顶点从没有入队,则说明没有从源点到该顶点的路径。

S P F A SPFA SPFA 的空间复杂度为 O ( V ) \mathcal{O}(V) O(V)。如果顶点的平均入队次数为 k k k,则 S P F A SPFA SPFA的时间复杂度为 O ( k E ) \mathcal{O}(kE) O(kE),对于较为随机的稀疏图,根据经验 k k k 一般不超过 4。

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e3 + 9;
const int M = 1e4 + 9;

struct edge{
    int v, w, fail;
    edge(){}
    edge(int _v, int _w, int _fail){
        v = _v;
        w = _w;
        fail = _fail;
    }
}e[M << 1];
int head[N], len;
void init(){
    memset(head, -1, sizeof(head));
    len = 0;
    
}

void add(int u, int v, int w){
    e[len] = edge(v, w, head[u]);
    head[u] = len++;
}

void add2(int u, int v, int w){
    add(u, v, w);
    add(v, u, w);
}

int n, m;
int dis[N];
bool vis[N];

void spfa(int u){
    memset(vis, false, sizeof(vis));
    vis[u] = true;
    memset(dis, 0x3f, sizeof(dis));
    dis[u] = 0;
    queue<int> q;
    q.push(u);
    while(!q.empty()){
        u = q.front();
        q.pop();
        vis[u] = false;
        for(int j = head[u];~j;j = e[j].fail){
            int v = e[j].v;
            int w = e[j].w;
            if(dis[v] > dis[u] + w){
                dis[v] = dis[u] + w;
                if(!vis[v]){
                    q.push(v);
                    vis[v] = true;
                }
            }
        }
    }
}

int main() {
    
    init();
    int u, v, w;
    cin>>n>>m;
    while(m--){
        cin>>u>>v>>w;
        add2(u, v, w);
    }
    spfa(1);
    cout<<dis[n]<<endl;
    
    return 0;
}
②、 S P F A SPFA SPFA判断负环

D i j k s t r a Dijkstra Dijkstra不能处理有负权的图,而 S P F A SPFA SPFA 可以处理任意不含负环(负环是指总边权和为负数的环)的图的最短路,并能判断图中是否存在负环

但是 S P F A SPFA SPFA可以用来判断负环,在进行 S P F A SPFA SPFA时,用一个数组 c n t i cnt_i cnti来标记每个顶点入队次数。如果一个顶点入队次数 c n t i cnt_i cnti大于顶点总数 n,则表示该图中包含负环。一般情况下, S P F A SPFA SPFA 判负环都只用在有向图上,因为在无向图上,一条负边权的边就是一个负环了

memset(in, 0, sizeof in);
in[u] = 1;
// 修改入队部分的操作
if(!vis[v]){
    q.push(v);
    vis[v] = true;
    ++in[v];
    if(in[v] > n){
        return true;
    }
}

3、 F l o y d Floyd Floyd多源最短路算法

∀ 1 ≤ k ≤ n , d p [ i ] [ j ] = m i n ( d p [ i ] [ j ] , d p [ i ] [ k ] + d p [ k ] [ j ] ) ∀1≤k≤n,dp [i] [j] = min(dp[i] [j],dp[i][k]+ dp [k][j]) 1kn,dp[i][j]=mindp[i][j]dp[i][k]+dp[k][j]

int g[N][N];
void floyd(int n) {
    for (int k = 1; k <= n; ++k) {
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= n; ++j) {
                g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
            }
        }
    }    
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值