最短路模板


一、单源最短路

求从一个点到其他所有点的最短距离。

分为两大类:

1、所有边权都是正数(n 个点,m 条边)

  1. 朴素版的Dijkstra算法,时间复杂度为 O(n^2) ,适合稠密图(边多,点少边比较多)
  2. 堆优化版的Dijkstra算法,时间复杂度为 O(mlogn),适合稀疏图(点多,指边相对于点不多,m和n是同一个级别的类型)

2、存在负权边

  1. Bellman-Ford 算法,时间复杂度为 O(nm)
  2. SPFA 算法,时间复杂度一般为 O(m),最坏情况为 O(nm),是Bellman-Ford算法的优化

(1)朴素版 Dijkstra算法

稠密图用邻接矩阵,稀疏图用邻接表

1.逐个遍历,找到与起点最近的且未确定最短路径的点,访问加入集合并标记。

2.更新第一个点到起点的最短距离,直到第n个点。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;

int n, m;//点数和边数
int g[N][N];//邻接矩阵存图
int dist[N];//各个节点到起点的距离
bool st[N];//默认为0,判断是否找到了最短距离

int dijkstra(){
    //距离都初始化为无穷大
    memset(dist, 0x3f, sizeof(dist));
    dist[1] = 0;//第一个点,距离初始化为0
    
    //迭代n次
    for (int i = 0; i < n; i++){
        //t是距离最近的未确定的最短路点的编号
        int t = -1;//未加入最短路的点编号初始化为-1
        
        //遍历n个点
        //先找到一个未加入集合中的距离最近的点
        for (int j = 1; j <= n; j++){
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        }
        
        st[t] = true;//标记为已加入到集合中
        
        //更新到第j个点的最短路
        //用1到t的加t到j的距离更新1到j的距离
        for (int j = 1; j <= n; j++){
            dist[j] = min(dist[j], dist[t] + g[t][j]);
        }
    }
    
    //如果为无穷,说明不连通,无法形成最短路
    if (dist[n] == 0x3f3f3f3f)
        return -1;
    
    return dist[n];
}

int main(){
    scanf("%d%d", &n, &m);
    
    //邻接矩阵初始化为无穷大
    memset(g, 0x3f, sizeof(g));
    
    while (m--){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        
        //存入a和b两点中间的距离(有向图)
        g[a][b] = min(g[a][b], c);
        //无向图
        //g[a][b] = min(g[a][b], c);
        //g[b][a] = min(g[a][b], c);
    }
    
    int t = dijkstra();
    
    printf("%d\n", t);
    
    return 0;
}

(2)堆优化版的Dijkstra算法

稀疏图改用邻接表的形式存储,可以不需要考虑重边

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
#define PII pair<int, int>
const int N = 1e6 + 10;

int n, m;
//稀疏图用邻接表来存
//依次存储 头节点,权重,值,下一节点,序号
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];//初始为0

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

int dijkstra(){
    memset(dist, 0x3f, sizeof(dist));
    dist[1] = 0;
    
    //定义一个小根堆
    priority_queue<PII, vector<PII>, greater<PII> > heap;
    
    //这个顺序不能倒,pair排序时是先根据first,再根据second,这里要根据距离排序
    heap.push({0,1});
    
    while (heap.size()){
        auto t = heap.top();//取不在集合中的距离最短的点
        heap.pop();//取出来后弹出队列
        
        //ver为当前节点编号
        int ver = t.second, distance = t.first;
        if (st[ver]) continue;
        st[ver] = true;
        
        for (int i = h[ver]; i != -1; i = ne[i]){
            int j = e[i];//e[]中存的是下标为i时所对应的点
            
            //判断最短的距离
            if (dist[j] > dist[ver] + w[i]){
                dist[j] = dist[ver] + w[i];
                heap.push({dist[j], j});
            }
        }
    }
    
    if (dist[n] == 0x3f3f3f3f)
        return -1;
    return dist[n];
}

int main(){
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof(h));
    
    while (m--){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    
    printf("%d\n", dijkstra());
    
    return 0;
}

(3)Bellman-ford 算法

1.可以用结构体存储点和边,包括负权边。

2.具体步骤:两重 for 循环,迭代 n-1 次,每次备份一下,每次循环遍历所有边,更新两点之间的最短距离,如点 a->b 的更新方式为(松弛操作)

for n 次
	for 所有边 a, b, w (松弛操作)
		dist[b] = min(dist[b], backup[a] + w);

backup[ ] 数组是上一次迭代后 dist[ ] 数组的备份,由于是每个点同时向外出发,因此需要对 dist[ ] 数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点。

3.循环 n-1 次之后,对于所有的点都 一定满足 dist[b] <= dist[a] + w ,该式被称为三角不等式。

4.如果图中存在负权回路,那么最短路可能为负无穷。(不是一定)

5.是否能到达n号点的判断中需要进行 if(dist[n] > INF/2) 判断,而并非是 if(dist[n] == INF) 判断,原因是 INF 是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与 INF 相同数量级的数即可。

6.bellman-ford算法擅长解决有边数限制的最短路问题。

#include <iostream>
#include <cstring>
#include <algoritnm>
using namespace std;
#define INF 0x3f3f3f3f

const int N = 510, M = 10010;
    
int n, m, k;//点数、边数、限制的最多次查找的边数
int dist[N];//存储权重
int backup[N];//backup存储上一次迭代的结果

struct Edge
{
    int a, b, w;//两点及其权重
}edges[M];

int bellman_ford()
{
	//初始化
    memset(dist, INF, sizeof(dist));
    dist[1] = 0;
    
    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);
        }
    }
}

int main(){
    scanf("%d%d%d", &n, &m, &k);
    
    for (int i = 0; i < m; i++){
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        
        edges[i] = {a, b, w};
    }
    
    bellman_ford();
    
    if (dist[n] > INF / 2)
        puts("impossible");
    else 
        printf("%d\n", dist[n]);
    
    return 0;
}

(4)SPFA算法

1.用队列来存储

2.while queue 不为空,

  1. 取出作为 t ,t = q.front; q.pop();
  2. 更新 t 的所有出边,如:t -> b, 把b加入 queue

3.基本步骤

  • 建立一个队列,初始时队列里只有起始点
  • 再建立一个数组记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到它本身的路径赋为0)
  • 再建立一个数组,标记点是否在队列中
  • 队头不断出队,计算起始点经过队头到其他点的距离是否变短,如果变短且该点不在队列中,则把该点加入到队尾
  • 重复执行直到队列为空
  • 在保存最短路径的数组中,就得到了最短路径
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010;

int n, m;
int h[N], e[N], w[N], ne[N], idx;//邻接表存图
int dist[N];
int 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, 0x3f, sizeof(dist));
    dist[1] = 0;//1号到1号的距离为0
    
    queue<int> q;
    q.push(1);//从1号开始入队
    st[1] = true;//标记1号顶点在队列中
    
    while (q.size()){
        int t = q.front();//取出队头顶点赋给t
        q.pop();
        st[t] = false;//取完队头后,t不在队列中了
        
        //遍历所有和t相连的点
        for (int i = h[t]; i != -1; i = ne[i]){
            int  j = e[i];
            
            //如果可以使距离变得更短,则更新距离
            if (dist[j] > dist[t] + w[i]){
                //更新距离
                dist[j] = dist[t] + w[i];
                
                //如果不在队列中,将其入队,标记为已入队
                if (!st[j]){
                    q.push(j);
                    st[j] = true;
                }
            }
        }
        //后面如此循环,寻找下一个队头顶点,直到队列为空
    }
    
    return dist[n];
}

int main(){
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof(h));
    
    while (m--){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        
        add(a, b, c);//加入到邻接表
    }
    
    int t = spfa();
    
    if (t == 0x3f3f3f3f)//不能到达
        puts("impossible");
    else 
        printf("%d\n", t);
    
    return 0;
}

二、多源汇最短路

起点和终点不确定,求多个起点到多个终点的最短距离

Floyd 算法

1.使用邻接矩阵存图

2.三重循环,时间复杂度O(n^3)

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]);
//循环之后 d[i][j] 存储的就是i到j的最短距离

3.示例模板(如果出现SF错误,可以通过 删代码 确定错误地方)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210, INF = 1e9;

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]);
	//更新i到j的最短距离
}

int main(){
    //初始化输入 点数、边数、询问次数
    scanf("%d%d%d", &n, &m, &q);
    
    //初始化d[i][j]
    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, c;
        scanf("%d%d%d", &a, &b, &c);
        
        //保存最小的边
        d[a][b] = min(d[a][b], c);
    }
    
    floyd();
    
    //q次询问,询问a点和b点的最短距离
    while (q--){
        int a, b;
        scanf("%d%d", &a, &b);
        
        int t = d[a][b];
        //因为有负权边存在
        if (t > INF / 2)
            puts("impossible");
        else 
            printf("%d\n", t);
    }
    
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BraumAce

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值