图的最短路径算法

本文详细介绍了图的最短路径算法,包括单源最短路和多源最短路问题。讲解了Dijkstra算法、Bellman-ford算法、SPFA算法以及Floyd算法的原理、优缺点和优化策略,同时探讨了次短路问题的解决方法。
摘要由CSDN通过智能技术生成

图的存储

图:点集、边集。

我们程序中所有存储的信息是边的信息,点的信息没有必要存。

存储方式:

  1. 邻接矩阵(缺点:浪费储存空间。优点:可以快速索引两个点之间边的信息)
  2. 邻接表(c++用动态数组实现,c用链表实现)邻接表的思想是,对于图中的每一个顶点,用一个数组来记录这个点和哪些点相连。优点:节省存储空间,有几条边的信息就用几个存储空间。缺点:不好查找边的信息O(n)。
  3. 链式前向星(本质是邻接表)。当新增加一条边时,数据变动的方式其实就是链表的头插法。

最短路问题

  1. 单源最短路问题。(单一源点到其他点的最短路径问题)。
  2. 多源最短路径,任意两点之间的最短路径。

单源最短路算法

Dijkstra算法

解决单源最短路问题。但是它处理不了带负权值的图。因为带负权值的边可能会更新已经被扩展过的点,从而导致那时从该点往外扩展的权值都是错误的。

算法流程:

我们定义图中所有的顶点集合为V,已经确定从源点出发的最短路径的顶点集合为U,初始集合U为空。初始每个顶点到源点的距离为INF,源点到自身的距离为0。

  • 从v-u中找出一个距离源点最近(路径最短)的顶点v,将v加入集合U。

  • 然后根据这个最短路径来更新与顶点v相邻的、不在集合U中的顶点最短路径,这一步叫做松弛操作。

    • 重复步骤1和2,知道所有的点都归入集合U。

    我们乍一看像bfs,但是我们可以发现bfs每次是扩展一层,Dijkstra算法是每次扩展一个点。实质是广度优先搜索加贪心。

    核心思想就是一直维护一个还没有确定最短路径的点的集合,然后每次从这个集合中选出一个最小的点去更新其他的点。

    堆优化

    如果每次暴力枚举选取距离最小的元素,则总的时间复杂度是 O ( V 2 ) O(V^2) O(V2)。所以我们可以考虑用堆优化,用一个set来维护点的集合,这样时间复杂度就优化到了 O ( ( V + E ) l o g V ) O((V+E)logV) O((V+E)logV),常用来解决稠密图问题。

    算法需要两个数组,一个数组用来存储源点到图中所有点的距离值,另一个数组用来存储该点是否已经被用来扩展过了。

    算法的改进:当某个点的最短距离被更新,那么标记值就置为0。这样就能适用于带负权值的图。

    在时间复杂度上的改进:因为原始的算法,在每一次寻找新一轮的扩展点时,都要扫描一遍数组,所以时间复杂度为O(n^2)。但是我们可以拿优先队列来优化。在优先队列中存储一个pair对,存储边的编号和距离源点的距离,距离小的在队列头部。优化之后的时间复杂度为O(nlogn)。

    最短路的代码都是差不多的形式

    最简单版本的 D i j s k t r a Dijsktra Dijsktra算法:

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;

#define MAX_M 500000
#define MAX_N 10000
#define INF 0x3f3f3f3f

//图的存储一般采用链式前向星的方式储存
struct Edge {
   
   int to, next, c;
}g[MAX_M + 5]; //开的内存大小要跟边的数量相关
//head数组的值表示以当前下标为起始点的第一条边的结构体数组编号
int head[MAX_N + 5], cnt = 0;
int dis[MAX_N + 5], vis[MAX_N + 5];

//0编号是不存边的,因为我们用0表示结束位,从1开始存边
inline void add(int a, int b, int c) {
   
   //头插法
   g[++cnt].to = b;
   g[cnt].next = head[a];
   g[cnt].c = c;
   head[a] = cnt;
}

void dij(int s, int n) {
   
   memset(dis, 0x3f, sizeof(dis));
   dis[s] = 0;
   //扫描n-1轮
   for (int i = 1; i < n; i++) {
   
       int ind = -1;
       //找到未被标记的最短路径
       for (int j = 1; j <= n; j++) {
   
           if (vis[j]) continue;
           if (ind == -1 || dis[j] < dis[ind]) ind = j;
       }
       //结束是以该点为起点的所有边都查过了,标记位为0
       for (int j = head[ind]; j; j = g[j].next) {
   
           //检查从ind出发所能扩展的边的路径与原始值相比能否更新
           if (dis[g[j].to] > dis[ind] + g[j].c) {
   
               dis[g[j].to] = dis[ind] + g[j].c;
           }
       }
       vis[ind] = 1;
   }
   return;
}

int main() {
   
   int n, m, s;
   cin >> n >> m >> s;
   for (int i = 0;i < m; i++) {
   
       int a, b, c;
       cin >> a >> b >> c;
       //注意这里处理的是有向边,若为无向图,则要加一个add(b,a,c)
       add(a, b, c);
}
   dij(s, n);
   for (int i = 1; i <= n; i++) {
   
       i == 1 || cout << " ";
       if (dis[i] == INF) {
   
           cout << 2147483647;
       } else  {
   
           cout << dis[i];
       }
   }
   return 0;
}

使用优先队列改进和重标记的Dijkstra算法(可适用于负权边图)

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <queue>
#include <iostream>
using namespace std;
#define MAX_N 10000
#define MAX_M 500000
#define INF 0x3f3f3f3f

struct Edge{
   
    int to, next, c;
}g[MAX_M + 5];
//head数组储存的是以当前下标(或加1)为起点的第一条边对应的结束点
int head[MAX_N + 5];
int dis[MAX_N + 5], vis[MAX_N + 5];

struct Data {
   
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值