图论之最短路径算法

在这里插入图片描述

Dijkstra 算法

迪杰斯特拉算法(Dijkstra)是由荷兰计算机科学家狄克斯特拉于1959 年提出的,因此又叫狄克斯特拉算法。
是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。

算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止

朴素Dijkstra算法

https://www.acwing.com/problem/content/851/

在这里插入图片描述

变量解释

变量名含义
int g[N][N]g[i][j] i->j 的距离
int dis[N]dis[i] 1->i 的最短距离
bool vis[N]vis[i] 当前1->i 的最短距离是否确定
int n,mn个点,m条边

算法流程

  1. 初始化最短距离。起点到自己的最短距离为0,到其他点的距离为INF(0x3f3f3f3f)
  2. 找距离起点最近的点。在没有确定最短距离的点中,找到距离起点距离最近的一个点
  3. 标记。将2找到的最近的点标记为已经确定最短路径的点
  4. 松弛。用2找到的点,更新从起点到每一个点的最短距离,min(dis[j], dis[t] + g[t][j])
  5. 重复第2步,直至所有存在最短路径的点更新完毕
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510;
int g[N][N];        // g[i][j]  i->j 的距离
int dis[N];         // dis[i]   1->i 的最短距离
bool vis[N];        // vis[i]   当前1->i 的最短距离是否确定
int n,m;

int dijkstra() {
    memset(dis, 0x3f, sizeof dis);
    dis[1] = 0;
    
    for(int i = 0; i < n; i++) {        // 访问 n 次
        int t = -1;
        for(int j = 1; j <= n; j++) {   // 访问 n 个点
            if(!vis[j] && (t == -1 || dis[t] > dis[j]))
                t = j;
        }
        
        vis[t] = true;
        for(int j = 1; j <= n; j++)      //依次更新每个点所到相邻的点路径值
            dis[j] = min(dis[j], dis[t] + g[t][j]);
    }
    
     //如果第n个点路径为无穷大,不存在 1->n 最短路径
    if(dis[n] == 0x3f3f3f3f) return -1;
    return dis[n];
}

int main() {
    cin >> n >> m;
    memset(g, 0x3f, sizeof g);
    for(int i = 0; i < m ; i++) {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        g[x][y] = min(g[x][y],z);   //如果发生重边的情况则保留最短的一条边
    }
    
    cout << dijkstra() << endl;;
    return 0;
}

堆优化版本

https://www.acwing.com/problem/content/852/

在这里插入图片描述

分析

为什么说是用堆进行优化,因为在算法的第一步中,需要在没有确定最短路径的点中,找到从起点到该点距离最短的一个点。

如果不加优化,这一步是O(n)的时间复杂度,但是如果使用小根堆的话,就可以优化为O(1),随后的一个pop操作,这是O(log n)的。整体来看,第一步可以优化为O(log n)

随后,在进行松弛的过程中,最多有m条边,而入队操作的时间复杂度为O(log n),总体时间复杂度为O(m log n)

在这里插入图片描述

#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>

using namespace std;

const int N = 5e5 + 10;
int h[N], e[N], W[N], ne[N], idx;
int dist[N];
bool vis[N];
int n,m;

typedef pair<int,int> PII;

void add(int u,int v,int k) {
    e[idx] = v, W[idx] = k, ne[idx] = h[u], h[u] = idx++;
}

int dijkstra() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    priority_queue<PII, vector<PII>, greater<PII> > pq;     // 小根堆
    pq.push({0,1});
    
    while(!pq.empty()) {
        PII t = pq.top();
        pq.pop();
        
        int distance = t.first, ver = t.second;
        if(vis[ver]) continue;  // 排除当前节点已经找到最短路的情况
        vis[ver] = true;
        
        for(int i = h[ver]; i != -1 ; i = ne[i]) {
            int v = e[i];
            if(dist[v] > distance + W[i]) {
                dist[v] = distance + W[i];
                pq.push({dist[v],v});
            }
        }
    }
    
    if(dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

Dijkstra 算法 操作负权边的问题

在这里插入图片描述
因为Dijkstra算法每次都是在只在可以到达的没有确定最短路径的点中,找到一个最短的边,然后通过这个边,再去更新其他的点。

就上面这个图来说,当存在负权边时,得到的最短路径为[0,-1,3],但显然应该是[0,-1,1]

belloman-ford算法

https://www.acwing.com/problem/content/855/

在这里插入图片描述
Bellman - ford算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。

其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在 n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果

算法执行流程

在这里插入图片描述
在松弛操作之前,需要对原本的数组进行拷贝,确保我们在松弛操作的时候,使用的是上一步松弛操作的结果,负责就会出现串联效应

为什么说存在负环可能不存在最短路径

因为belloman-ford算法,求的是单源的最短路径问题,那么对于负环的问题,只要负环不能到达终点,那么负环其实是对结果没有影响的。因为从起点到终点,已经确定了最多走k条

但如果负环可以到达终点,那么任由负环一直循环下去,一定会把最短路径变为 -INF

在这里插入图片描述

Belloman-ford算法判断负环的方式

我们一共有n个点,那么对于一个点来说,我最多松弛n-1次就可以找到一个最短路径,那么当我们继续第n次松弛的时候,如果还可以进行松弛,那就说明存在负权的回路

判断路径是否可达

在这里插入图片描述
/2的原因是,0x3f3f3f3f是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与0x3f3f3f3f相同数量级的数即可。

另外,根据题目的数据范围,负权边最多可以影响500 * 10000 次,但是0x3f3f3f3f远远大于这个数,所以说以此来判断是否存在最短路
在这里插入图片描述

源程序

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510, M = 1e4 + 10;
int n,m,k;  // n个点,m条边,k 最多经过k条边
int dist[N], backup[N];     // dist[i] 从1->i 的最短距离,backup dist的一个副本

struct edges{
    int u,v,w;
}e[M];

int bellman_ford() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    for(int i = 0; i < k; i++) {
        // 副本
        memcpy(backup,dist,sizeof dist);
        
        for(int i = 0; i < m; i++) {
            int u = e[i].u, v = e[i].v, w = e[i].w;
            dist[v] = min(dist[v], backup[u] + w);
        }
    }
    
    if(dist[n] >= 0x3f3f3f3f / 2) return -1;
    return dist[n];
}

int main() {
    cin >> n >> m >> k;
    for(int i = 0; i < m; i++) {
        cin >> e[i].u >> e[i].v >> e[i].w;
    }
    
    int t = bellman_ford();
    
    if(t == -1) puts("impossible");
    else cout << t << endl;
    
    return 0;
}

spfa算法

https://www.acwing.com/problem/content/853/

在这里插入图片描述
在belloman-ford算法中,对于遍历的操作其实是有些多余的。因为对于那些没有更新的边,还是进行了一次判断,也就是判断了一些0x3f3f3f3f + w 的操作,显然是没有必要的
在这里插入图片描述
spfa算法在这个的基础上,对于松弛操作,利用宽度优先搜索做了一次优化,确保我们每次松弛的操作不会做一些无用功
在这里插入图片描述

时间复杂度 :n为点数,m为边数

一般:O(m) 最坏:O(nm)(网格图的形式时,边权为特殊数值)

#include <cstring>
#include <algorithm>
#include <queue>
#include <iostream>

using namespace std;

const int N = 1e5 + 10;
int h[N], e[N], w[N], ne[N], idx;
int dist[N];
bool vis[N];
int n,m;

void add(int u,int v,int k) {
    e[idx] = v, w[idx] = k, ne[idx] = h[u], h[u] = idx++;
}

int spfa() {
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    
    queue<int> que;
    que.push(1);
    vis[1] = true;
    
    while(!que.empty()) {
        int f = que.front();
        que.pop();
        
        vis[f] = false;
        
        for(int i = h[f]; ~i ; i = ne[i]) {
            int v = e[i];
            if(dist[v] > dist[f] + w[i]) {
                dist[v] = dist[f] + w[i];
                if(!vis[v]) {
                    que.push(v);
                    vis[v] = true;
                }
            }
        }
    }
    
    if(dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

int main() {
    cin >> n >> m;
    memset(h,-1,sizeof h);
    
    for(int i = 0; i < m; i++) {
        int a,b,c;
        cin >> a >> b >> c;
        add(a,b,c);
    }
    
    int t = spfa();

    if(t == -1) puts("impossible");
    else cout << t << endl;
    return 0;
}


判断负环

在判断负环的过程中,依据抽屉原理,我们统计每个点的访问次数,对于一个点,当他被访问了 n次的时候,也就是访问了n个边,即n + 1个点,这些点中肯定存在两个相同的点,所以说存在负环

判断是否存在负环,并非判断是否存在从1开始的负环,因此需要将所有的点都加入队列中,更新周围的点

返回true表示存在负环,false 表示不存在负环

bool spfa() {
    memset(dist,0x3f,sizeof dist);
    
    queue<int> que;
    for(int i = 1; i <= n; i++) {
        que.push(i);
        vis[i] = true;
    }
    
    while(!que.empty()) {
        int f = que.front();
        que.pop();
        
        vis[f] = false;
        
        for(int i = h[f]; ~i ; i = ne[i]) {
            int v = e[i];
            if(dist[v] > dist[f] + w[i]) {
                dist[v] = dist[f] + w[i];
                cnt[v] = cnt[f] + 1;
                if(cnt[v] >= n) return true;
                
                if(!vis[v]) {
                    vis[v] = true;
                    que.push(v);
                }
            }
        }
    }
    
    return false;
}

Floyd算法

在这里插入图片描述
f[i, j, k]表示从``i走到j的路径上除ij点外只经过1k的点的所有路径的最短距离

那么f[i, j, k] = min(f[i, j, k - 1), f[i, k, k - 1] + f[k, j, k - 1]

因此在计算第k层的f[i, j]的时候必须先将第k - 1层的所有状态计算出来,所以需要把k放在最外层

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 210, INF = 0x3f3f3f3f;
int dist[N][N];
int n,m,q;

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

int main() {
    cin >> n >> m >> q;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            if(i == j)  dist[i][i] = 0;
            else dist[i][j] = INF;
    
    while(m --) {
        int x,y,z;
        cin >> x >> y >> z;
        dist[x][y] = min(dist[x][y],z);
    }
    
    floyd();
    
    while(q--) {
        int x,y;
        cin >> x >> y;
        if(dist[x][y] >= INF / 2) puts("impossible");
        else cout << dist[x][y] << endl;
    }
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值