最短路问题

最短路

一、常见的最短路问题

(规定: n n n为点数, m m m为边数)

  1. 单源最短路:求一个点到其他所有点的最短距离
    1. 所有边权都是正数
      • 朴素 D i j k s t r a Dijkstra Dijkstra算法 O ( n 2 ) O(n^2) O(n2) 时间复杂度和边数无关,适合稠密图( m m m~ n 2 n^2 n2)
      • 堆优化版的 D i j k s t r a Dijkstra Dijkstra算法 O ( m l o g n ) O(mlogn) O(mlogn)
    2. 存在负权边
      • B e l l m a n − F o r d Bellman-Ford BellmanFord O ( n m ) O(nm) O(nm)
      • S P F A SPFA SPFA 一般 O ( m ) O(m) O(m) 最坏 O ( n m ) O(nm) O(nm)
  2. 多源汇最短路:源点(起点) 汇点(终点) 有多个询问,起点和终点不确定
    • F l o y d Floyd Floyd算法 O ( n 3 ) O(n^3) O(n3)
二、朴素 D i j k s t r a Dijkstra Dijkstra算法
//集合S:所有当前已确定了最短距离的点
step1: dist[1]=0,dist[i]=+∞
step2: for i : 1~n {//循环结束之后可以求出每个点到起点的最短路
    	  t <- 不在s中的,距离最近的点   (n^2次)
    	  s <- t
       	  用t来更新其他所有点的距离 //例如用1号点到x的距离 与 1->t->x再加上权重比		较,再用短的那个更新
	  }

给定一个 nn 个点 mm 条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出 11 号点到 nn 号点的最短距离,如果无法从 11 号点走到 nn 号点,则输出 −1−1。

输入格式

第一行包含整数 nn 和 mm。

接下来 mm 行每行包含三个整数 x,y,zx,y,z,表示存在一条从点 xx 到点 yy 的有向边,边长为 zz。

输出格式

输出一个整数,表示 11 号点到 nn 号点的最短距离。

如果路径不存在,则输出 −1−1。

数据范围

1≤n≤5001≤n≤500,
1≤m≤1051≤m≤105,
图中涉及边长均不超过10000。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
#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];//每个点的最短路是否已经确定

int dijkstra(){
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    
    for(int i=0;i<n;i++){//迭代n次,即可遍历所有的点
        int t=-1; //将t设置为-1,因为Dijkstra算法适用于不存在负权边的图
        for(int j=1;j<=n;j++){//寻找还未确定最短路的点中路径最短的点
            if(!st[j] && (t==-1 || dist[t]>dist[j]))
                t=j;
        }//最终循环结束后的t是还未确定点中距离最短的
        
        st[t]=true;//把这个点标记
        
        for(int j=1;j<=n;j++)//用t更新剩下未确定点的距离,之前的点即使遍历到了也不会改变其数值
            dist[j]=min(dist[j],dist[t]+g[t][j]);
    }
    
    if(dist[n]==0x3f3f3f3f) return -1;
    return dist[n];
}

int main(){
    cin>>n>>m;
    
    memset(g,0x3f,sizeof g);//初始化图,每个点最初为无限大
    
    while(m--){
        int a,b,c;
        cin>>a>>b>>c;
        g[a][b]=min(g[a][b],c);//重边的处理,如果a、b之间有多条边,则保留长度最短的那一条
    }
    
    int t=dijkstra();
    
    printf("%d\n",t);
    return 0;
}
二、堆优化版的 D i j k s t r a Dijkstra Dijkstra算法

每一次找不在S中的最短距离的点,可以用堆来寻找,时间复杂度,从 O ( n 2 ) O(n^2) O(n2) -> O ( 1 ) O(1) O(1)

此时更新距离的复杂度由 m m m次到 -> m l o g n mlogn mlogn次。

遍历所有点和所有边,即遍历所有边。

  1. 手写堆 n个数可任意修改
  2. s t l stl stl 优先队列 无法修改其中元素,每次修改都是重新插入 一般复杂度变为 O ( m l o g m ) O(mlogm) O(mlogm)

给定一个 nn 个点 mm 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。

请你求出 11 号点到 nn 号点的最短距离,如果无法从 11 号点走到 nn 号点,则输出 −1−1。

输入格式

第一行包含整数 nn 和 mm。

接下来 mm 行每行包含三个整数 x,y,zx,y,z,表示存在一条从点 xx 到点 yy 的有向边,边长为 zz。

输出格式

输出一个整数,表示 11 号点到 nn 号点的最短距离。

如果路径不存在,则输出 −1−1。

数据范围

1≤n,m≤1.5×1051≤n,m≤1.5×105,
图中涉及边长均不小于 00,且不超过 1000010000。
数据保证:如果最短路存在,则最短路的长度不超过 109109。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>

using namespace std;

typedef pair<int,int> PII;

const int N=1e6+10;

int n,m;
int h[N],e[N],ne[N],idx;
int w[N];//权重
int dist[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++;
}

int dijkstra(){
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    
    priority_queue<PII,vector<PII>,greater<PII>> heap;
    //这一行是定义优先队列,参数vector<PII>表示堆是用vector来实现的,greater是小根堆(小的数在上)
    heap.push({0,1});//把1号点放进去来更新剩下所有的点
    //堆里第一个元素表示距离dist,第二个元素是其在堆中的编号
    
    while(heap.size()){
        auto t=heap.top();
        heap.pop();
        
        int ver=t.second;
        if(st[ver]) continue;//这个点的最短距离已确定,跳过
        
        st[ver]=true;
        
        for(int i=h[ver];i!=-1;i=ne[i]){
            int j=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);//邻接表表头初始化,使h[i]均指向-1
    //邻接表不需要处理重边
    
    while(m--){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    
    cout<<dijkstra()<<endl;
    return 0;
}

三、 B e l l m a n − F o r d Bellman-Ford BellmanFord 算法
存边方式:定义一个结构体即可(未必需要是邻接矩阵/邻接表)
    struct{
        int a,b,w;
    }edge[M];//M条边

for 循环n次 
//循环k次后的dist[]数组表示,从1号点,经过不超过k条边,到每个点的最短距离
//若n次时dist[]又进行了更新,即存在一条最短路径,上有n条边,即n+1个点,则路径上存在环,因为更新所以是负环
    上一次dist[]备份到backup[],防止串联,用上一次迭代的结果去更新下一次
    for 循环所有边 a b w(存在一条a->b)的边,权重为w //第二层循环m次
        dist[b]=min(dist[b],backup[a]+w);//更新:比较1->a->b与1->b(松弛操作)
	//这里的a的距离,用的是上一次的,不能直接用dist[a],因为每一次迭代后的dist[a]可能不同

循环完之后,所有边一定满足:dist[b]<dist[a]+w (三角不等式)

如果有负权回路,最短路不一定必存在(可能为负无穷)。如果负环不在1->n的路径上,则有最短路径。

给定一个 nn 个点 mm 条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你求出从 11 号点到 nn 号点的最多经过 kk 条边的最短距离,如果无法从 11 号点走到 nn 号点,输出 impossible

注意:图中可能 存在负权回路

输入格式

第一行包含三个整数 n,m,kn,m,k。

接下来 mm 行,每行包含三个整数 x,y,zx,y,z,表示存在一条从点 xx 到点 yy 的有向边,边长为 zz。

点的编号为 1∼n1∼n。

输出格式

输出一个整数,表示从 11 号点到 nn 号点的最多经过 kk 条边的最短距离。

如果不存在满足条件的路径,则输出 impossible

数据范围

1≤n,k≤5001≤n,k≤500,
1≤m≤100001≤m≤10000,
1≤x,y≤n1≤x,y≤n,
任意边长的绝对值不超过 1000010000。

输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3
//由于已经限制了最多经过k条边,故只能用Bellman-Ford算法
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N=510,M=10010;

int n,m,k;
int dist[N],backup[N];

struct Egde{
    int a,b,w;
}edges[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 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);
        }
    }
    
    if(dist[n] > 0x3f3f3f3f/2) return 0;
    //为什么不写成dist[n]==0x3f3f3f3f?没有最短路时可能dist[n]=0x3f3f3f3f-w
    return dist[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);
        edges[i]={a,b,w}; //结构体的赋值
    }
    
    int t=bellman_ford();
    
    if(t==0) puts("impossible");
    else printf("%d\n",t);
}
四、 S P F A SPFA SPFA算法
Bellman Ford 每次循环未必会更新距离,而spfa对这里进行了优化(使用宽搜)
因为dist[b]=min(dist[b],dist[a]+w)
所以只有dist[a]减小,dist[b]才会减小
    
queue <- 1 (变小的节点存入队列)
while queue不空
    t <- q.front()
    q.pop()
    //如果一个点被更新过,再拿他来更新别人
    更新t的所有出边 t->b=w
    queue <- b

给定一个 nn 个点 mm 条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你求出 11 号点到 nn 号点的最短距离,如果无法从 11 号点走到 nn 号点,则输出 impossible

数据保证不存在负权回路。

输入格式

第一行包含整数 nn 和 mm。

接下来 mm 行每行包含三个整数 x,y,zx,y,z,表示存在一条从点 xx 到点 yy 的有向边,边长为 zz。

输出格式

输出一个整数,表示 11 号点到 nn 号点的最短距离。

如果路径不存在,则输出 impossible

数据范围

1≤n,m≤1051≤n,m≤105,
图中涉及边长绝对值均不超过 1000010000。

输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;

typedef pair<int,int> PII;

const int N=+1e6+10;

int n,m;
int h[N],e[N],ne[N],idx;
int w[N];
int dist[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++;
}

int spfa(){
   memset(dist,0x3f,sizeof dist);
   dist[1]=0;
   
   queue<int> q;//队列中存的是更新过的点
   q.push(1);//第一个点存入队列
   st[1]=true;//st[]储存这个点是否在队列中,防止重复添加
   
   while(q.size()){
       int t=q.front();
       q.pop();
       
       st[t]=false;//取出t
       
       for(int i=h[t];i!=-1;i=ne[i]){//遍历t的所有出边
           int j=e[i];
           if(dist[j]>dist[t]+w[i]){//如过dist需要改变
               dist[j]=dist[t]+w[i];
               if(!st[j]){
                   q.push(j);
                   st[j]=true;
               }
           }
       }
   }
   if(dist[n]==0x3f3f3f3f) return 0;
   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==0) puts("impossible");
    else printf("%d\n",t);
    return 0;
}

spfa判断负环

dist[x]最短距离
cnt[x]边数
dist[x]=dist[t]+w[t]
cnt[x]=cnt[t]+1
    
如果cnt[x]>=n 则图中存在负环

给定一个 nn 个点 mm 条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你判断图中是否存在负权回路。

输入格式

第一行包含整数 nn 和 mm。

接下来 mm 行每行包含三个整数 x,y,zx,y,z,表示存在一条从点 xx 到点 yy 的有向边,边长为 zz。

输出格式

如果图中存在负权回路,则输出 Yes,否则输出 No

数据范围

1≤n≤20001≤n≤2000,
1≤m≤100001≤m≤10000,
图中涉及边长绝对值均不超过 1000010000。

输入样例:
3 3
1 2 -1
2 3 4
3 1 -4
输出样例:
Yes
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;

typedef pair<int,int> PII;

const int N=+1e6+10;

int n,m;
int h[N],e[N],ne[N],idx;
int w[N];
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(){
   memset(dist,0x3f,sizeof dist);
   dist[1]=0;
   
   queue<int> q;//队列中存的是更新过的点
   //不能只将1号点存入队列,因为负环未必是从1号点开始的
   //所以把所有点放入对列
   for(int i=1;i<=n;i++)
    q.push(i);
   
   while(q.size()){
       int t=q.front();
       q.pop();
       
       st[t]=false;//取出t
       
       for(int i=h[t];i!=-1;i=ne[i]){//遍历t的所有出边
           int j=e[i];
           if(dist[j]>dist[t]+w[i]){//如过dist需要改变
               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;
}
五、 F l o y d Floyd Floyd算法
d[i,j] 邻接矩阵存储边
    
    for(k=1;k<=n;k++)
        for(i=1;i<=n;i++)
            for(j=1;j<=n;j++)
                d[i,j]=min(d[i,j],d[i,j]+d[i,k]+d[k,j]);

循环结束之后。d[i,j]存储的是i到j的最短路

原理:基于动态规划
d[k,i,j] 从i点。只经过1-k这些中间点到达j的最短距离
    d[k,i,j]=d[k-1,i,k]+d[k-1,k,j]

给定一个 nn 个点 mm 条边的有向图,图中可能存在重边和自环,边权可能为负数。

再给定 kk 个询问,每个询问包含两个整数 xx 和 yy,表示查询从点 xx 到点 yy 的最短距离,如果路径不存在,则输出 impossible

数据保证图中不存在负权回路。

输入格式

第一行包含三个整数 n,m,kn,m,k。

接下来 mm 行,每行包含三个整数 x,y,zx,y,z,表示存在一条从点 xx 到点 yy 的有向边,边长为 zz。

接下来 kk 行,每行包含两个整数 x,yx,y,表示询问点 xx 到点 yy 的最短距离。

输出格式

共 kk 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible

数据范围

1≤n≤2001≤n≤200,
1≤k≤n21≤k≤n2
1≤m≤200001≤m≤20000,
图中涉及边长绝对值均不超过 1000010000。

输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1
#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]);
}

int main(){
    scanf("%d%d%d",&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;

    while(m--){
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);

        d[a][b]=min(d[a][b],w);//若有重边,则保留最短的那一条
    }

    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;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值