最短路问题

最短路问题 - 模板总结(Dijkstra + Bellman-Ford + SPFA + Floyd)

本文参考原网站https://www.it610.com/article/1282949950665146368.htm
在这里插入图片描述

*1、Dijkstra算法(正边权单源最短路问题) *

1-1、朴素Dijkstra算法-O(n2)

给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出-1。
数据范围
1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
思路:每次把到起点的最短距离的点加入点集

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn = 510;
int n,m;
int g[maxn][maxn],d[maxn];
bool vis[maxn];
int dijkstra(){
    memset(d,0x3f,sizeof d);//初始距离不可达
    d[1] = 0;
    for(int i = 0;i < n;i++){//遍历n次
        int t = -1;
        for(int j = 1;j <= n;j++){
            if(!vis[j] && (t == -1 || d[t] > d[j])) t = j;//找点集外初始最短边
        }
        vis[t] = true;//加入点集
        for(int j = 1;j <= n;j++){
            if(!vis[j] && d[t] + g[t][j] < d[j])    d[j] = d[t] + g[t][j];//松弛操作
        }
    }
    if(d[n] == 0x3f3f3f3f)  return -1;//终点不可达
    return d[n];
}
int main(){
    scanf("%d%d",&n,&m);
    int a,b,c;
    memset(g,0x3f,sizeof g);
    while(m--){
        scanf("%d%d%d",&a,&b,&c);
        g[a][b] = min(g[a][b],c);
    }
    cout<<dijkstra()<<endl;
    return 0;
}

1-2.堆优化的Dijkstra算法-O(mlogn)

给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出-1。
数据范围
1≤n,m≤1.5×105,
图中涉及边长均不小于0,且不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3

思路:这题的数据范围要1e5+,所以用邻接矩阵存图肯定是不行的,我们采用邻接表的链式前向星
简单介绍下链式前向星存图:

void add(int x,int y,int z) {
 e[cnt] = y; //e[]代表临边 
 w[cnt] = z; //w[]代表权重 
 ne[cnt] = h[x];  //ne[] 指向下一条边,若ne[x]!=-1,则x有多条边 
 h[x] = cnt++; //定位第一条边 
}
 for(int i = h[k]; i ~; i = ne[i]) { 
//遍历
  }

ac代码

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int,int>pll;
const int maxn = 1e6+5;
int w[maxn],e[maxn],h[maxn],nxt[maxn];
int dis[maxn],cnt,x,y,z,n,m;
bool vis[maxn];
priority_queue<pll,vector<pll>,greater<pll> >q; // 这里heap中为什么要存pair呢,首先小根堆是根据距离来排的,所以有一个变量要是距离,其次在从堆中拿出来的时    
    // 候要知道知道这个点是哪个点,不然怎么更新邻接点呢?所以第二个变量要存点。
void add(int x,int y,int z) {
 e[cnt] = y;
 w[cnt] = z;
 nxt[cnt] = h[x];
 h[x] = cnt++;
}
int dijstra(int n){
 memset(dis,0x3f,sizeof dis);
    q.push({0,1});
    while(q.size()){
     pll k = q.top(); q.pop();
     int v = k.second; int d = k.first;
     if(vis[v]) continue;
      vis[v] = true;
      for(int i = h[v];i != -1; i = nxt[i]) {
       int j = e[i];
        if(dis[j] > d + w[i]) {
        dis[j] = d + w[i];
        q.push({dis[j],j});
        }
     }
   }
   if(dis[n] == 0x3f3f3f3f) return -1;
   else return dis[n];
}
int main()
{
    cin >> n >> m;
    memset(h,-1,sizeof h);
    for(int i = 0;i < m ; ++i){
    cin >> x >> y >> z;
    add(x,y,z);
    }
    cout << dijstra(n) << endl;
    return 0;
}

2.Bellman-Ford算法(带负权且经过的边的数量有限制)-O(nm)

给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从1号点到n号点的最多经过k条边的最短距离,如果无法从1号点走到n号点,输出impossible。
注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数n,m,k。
接下来m行,每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示从1号点到n号点的最多经过k条边的最短距离。
如果不存在满足条件的路径,则输出“impossible”。
数据范围
1≤n,k≤500,
1≤m≤10000,
任意边长的绝对值不超过10000。
输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3
思路: 有负权回路,最短路未必存在(起点到终点的路径上未经过负权环,此时仍然是有最短路的)。
共 迭 代 k 次 , 每 次 遍 历 所 有 的 边 , 更 新 所 有 点 到 起 点 的 最 短 距 离 。

#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
const int maxn = 10010;
struct node{
     int a,b,w;
}e[maxn];
int back[maxn],dis[maxn];
int n,m,k;
int bf() {
     memset(dis,0x3f,sizeof dis);
     dis[1] = 0;
     for(int i = 0;i < k ;++i) {
       memcpy(back,dis,sizeof dis);
         for(int j = 0;j < m ; ++j) {
           int a = e[j].a,b = e[j].b , w = e[j].w;
           dis[b] = min(dis[b],back[a] + w);
         }
     }
     if(dis[n] > (0x3f3f3f3f) / 2) return -1;
     else return dis[n]; 
}
int main(){
     cin >> n >> m >> k;
     for(int i = 0; i < m; ++i) {
     int a,b,w;
     scanf("%d%d%d",&a,&b,&w);
     e[i] = {a,b,w};
}
     int t = bf();
     if(t == -1) printf("impossible\n");
     else printf("%d\n",t);
     return 0;
}

3.SPFA算法(带负权的最短路)-O(m)~O(nm)

3-1、SPFA求最短路

给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出impossible。
数据保证不存在负权回路。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出”impossible”。
数据范围
1≤n,m≤105
图中涉及边长绝对值均不超过10000。
输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2
思路:
S P F A 算 法 是 B e l l m a n − F o r d 算 法 的 优 化 , 我 们 发 现 B F 算 法 每 次 迭 代 均 要 遍 历 所 有 的 边 , 但 事 实 上 , 并 不 是 所 有 边 均 会 被 更 新 得 更 小 。 转 移 方 程 : d i s [ b ] = m i n ( d i s [ b ] , d i s [ a ] + w ) , 可 见 , 只 有 当 d i s [ a ] 减 小 , d i s [ w ] 才 会 变 小 。 因 此 , 只 有 当 某 个 点 到 起 点 的 距 离 变 小 , 经 过 该 节 点 的 其 他 点 到 起 点 的 距 离 才 可 能 变 小。
我 们 采 用 B F S 的 思 想 来 对 B F 算 法 进 行 优 化 , 用 一 个 队 列 来 存 储 到 起 点 距 离 变 短 的 点 , 每 次 出 队 后 用 该 点 更 新 与 其 相 邻 节 点 到 起 点 的 距 离 , 再 将 更 新 过 的 到 起 点 距 离 变 短 的 点 继 续 入 队 。需 要 注 意 , 由 于 存 在 负 权 边 , 因 此 我 们 初 始 化 的 + ∞ 可 能 会 略 微 的 减 小 。 因 此 我 们 认 为 若 某 点 到 起 点 的 距 离 大 于 + ∞ 2 , 则 说 明 从 起 点 无 法 到 达 该 点

#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int maxn = 1e5 + 5;
int dis[maxn],e[maxn],w[maxn],h[maxn],ne[maxn];
int x,y,z,cnt,n,m;
bool vis[maxn];
void add(int x,int y,int z) {
 e[cnt] = y; //e[]代表临边 
 w[cnt] = z; //w[]代表权重 
 ne[cnt] = h[x];  //ne[] 指向下一条边,若ne[x]!=-1,则x有多条边 
 h[x] = cnt++; //定位第一条边 
}
int spfa(int n){
    memset(dis,0x3f,sizeof dis);
    queue<int>q;
    q.push(1);
    while(q.size()){
     int k = q.front(); q.pop();
     dis[1] = 0;
     vis[k] = false;
     for(int i = h[k]; i != -1; i = ne[i]) { //遍历与k相连的每一条边 
      int j = e[i];
      if(dis[j] > dis[k] + w[i]) {
       dis[j] = dis[k] + w[i];
       if(!vis[j]) {
        vis[j] = true;
        q.push(j);
       }
      }
     }
    }
   if(dis[n] == 0x3f3f3f3f) return -1;
   else return dis[n];
}
int main(){
     cin >> n >> m ;
     memset(h,-1,sizeof h);
     while(m--) {
     cin >> x >> y >> z;
     add(x,y,z);
 }
    if(spfa(n) == -1) printf("impossible\n");
    else cout << dis[n] << endl;
    return 0;
}
3-2、SPFA判断负环

给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
如果图中存在负权回路,则输出“Yes”,否则输出“No”。
数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过10000。
输入样例:
3 3
1 2 -1
2 3 4
3 1 -4
输出样例:
Yes

思路:何为负环?即成环且环的总权值为负数。与 求 最 短 路 的 思 路 类 似 , 需 额 外 增 加 数 组 c n t [ x ] , 记 录 起 点 到 x 点 经 过 的 边 数 。
若 c n t [ x ] > = n , 表 示 从 起 点 到 x 点 经 过 了 n 条 边 , 意 味 着 经 过 了 n + 1 个 点 , 由 抽 屉 原 理 , 必 然 有 某 个 点 经 过 了 2 次 。 这 就 说 明 了 图 中 存 在 一 个 负 环 。
与 最 短 路 不 同 的 是 , 这 次 我 们 要 先 将 所 有 点 入 队 。 因 为 负 环 可 能 从 某 个 起 点 出 发 未 必 能 经 过 , 需 要 将 所 有 点 都 当 作 起 点 跑 S P F A 。
Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。

#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 2010, M = 10010;
int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N];
void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool spfa()
{
    queue<int> q;
    for (int i = 1; i <= n; i ++ )
    {
        st[i] = true;
        q.push(i);
    }
    while (q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = false;
        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];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;
}
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);
    }
    if (spfa()) puts("Yes");
    else puts("No");
    return 0;
}

4、Floyd(多源最短路)-O(n3)

给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定k个询问,每个询问包含两个整数x和y,表示查询从点x到点y的最短距离,如果路径不存在,则输出“impossible”。
数据保证图中不存在负权回路。
输入格式
第一行包含三个整数n,m,k
接下来m行,每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
接下来k行,每行包含两个整数x,y,表示询问点x到点y的最短距离。
输出格式
共k行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出“impossible”。
数据范围
1≤n≤200,
1≤k≤n2
1≤m≤20000,
图中涉及边长绝对值均不超过10000。
输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1

思路:
思 路 十 分 简 单 , 枚 举 中 间 点 k , 更 新 所 有 从 i 到 j 且 经 过 k 的 路 径 的 最 短 距 离 。
同 样 的 , 由 于 存 在 负 权 边 , 若 距 离 仍 大 于 + ∞ 2 , 则 认 为 两 点 之 间 不 连 通 。

#include<iostream>
#include<queue>
#include<cstring>
#define inf 0x3f3f3f3f
using namespace std;
const int N=210;
int n,m,q,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]);
} 
int main()
{
    cin>>n>>m>>q;
    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;
            
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        d[a][b]=min(d[a][b],c);
    }
    floyd(); 
    while(q--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        if(d[a][b]>inf/2) puts("impossible");
        else printf("%d\n",d[a][b]);
    }
    return 0;
}

对比4种算法

结点n,边m边权值选用算法
n<200允许有负floyd
n*m<1e7允许有负bellman-ford
更大有负spfa
更大无负dijkstra
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值