最短路算法总结

最短路算法总结 2023.3.15

最短路的概念

在一个图中有 n个点、m条边。边有权值,权值可正可负。边可能是有向的,也可能是无向的。给定两个点,起点是s,终点是t,在所有能连接s和t的路径中寻找边的权值之“和” 最小的路径,这就是最短路径问题。如下图:求v1->v6的最短距离。

最短路类型

单源最短路:从单个节点出发,到所有节点的最短路
多源最短路:整个图中所有点到其他点的最短路

对于无权图的最短路的求法,是一个经典的搜索结构。最简单的方式是通过BFS逐个节点进行遍历。

 

对于有权图最短路的求法

 对于有权的一个图,需要注意边权正负或是否存在负权回路,根据题目不同选择合适的处理办法。

一、Floyd最短路算法

  1)基本思想:对于任意一条s->t的路径,必定存在一个点k处于该路径上(k可能与s,t相等)。原型dist[k][i][j]表示找i和j之间通过编号不超过k(k从1到n)的节点的最短路径。推导公式为:dist[k][i][j] = min(dist[k-1][i][j] , dist[k-1][i][k]+dist[k-1][k][j]),最终优化为:dist[i][j]=min(dist[i][k]+dist[k][j],dist[i][j]),可以用三角形判定法理解。

  2)在Floyd算法枚举ki​的时候,已经得到了前 k-1 个点的最短路径,这 k-1 个点不包括点 k,并且他们的最短路径中也不包括 k 点

  3)三层for循环即可实现,需要注意的是,最外层一定是枚举k,即要先求出任意两点i,j通过点k得到的最短距离。

  4)floyd是一个多源最短路径算法,即经过一次floyd后能求出任意两点间的最短路

  5)可以适用于没有负权回路的图,复杂度为:O(m3)

floyd模板

复制代码
#include<bits/stdc++.h>
using namespace std;
int g[205][205];
int main(){
    memset(g,0x3f,sizeof g);
    int n,m,k;cin>>n>>m>>k;
    while(m--){
        int x,y,z;cin>>x>>y>>z;
        g[x][y]=z;
    }
    
    //最短路径的处理,floyd 佛洛依德最短路办法
    for(int p=1;p<=n;p++){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                g[i][j]=min(g[i][p]+g[p][j],g[i][j]);
            }
        } 
    }
    while(k--){
        int x,y;cin>>x>>y;
        if(g[x][y]==0x3f3f3f3f) cout<<"impossible"<<endl;
        else cout<<g[x][y]<<endl; 
    }
    return 0;
}
复制代码

二、SPFA最短路算法

  1)基本思想:初始化起点s的距离为0,其他点的距离为正无穷,将所有与s相连的ki点入队,,如果点ki的距离发生变化,更新点ki的距离,并将点ki重新入队。直到所有点距离不再更新,此时求得最短路。

  2)如果存在一个负权环,那环上的点会被多次入队。利用这个特性做判定,如果某个入队次数大于等于n,那必然存在负权环。(对于某个点,最多有n-1个点使它距离变小,每次距离变小便入队一次,所以最多入队n-1次)

  3)存在负权边或负权回路依然可用。因为存在一个点多次入队的情况,所以最坏的情况复杂度为O(n*m),最好的情况为O(m),容易被卡。

spfa模板

复制代码
//SPFA算法 
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,s;
int head[N];//当前点指向的边 
int ne[N<<1];//当前边的上一条边 
int ver[N<<1];//当前边的重点 
int w[N<<1];//当前边的权值 
int tot;//边的个数 
void add(int x,int y,int z){//邻接表存边,也是图的通常存储应用 
    ver[++tot]=y,w[tot] =z;
    ne[tot]=head[x],head[x]=tot;
}

queue<int> q;
int dis[N];
void spfa(){
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    q.push(1);
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=head[x];i;i=ne[i]){
            int y=ver[i];
            if(dis[y]>dis[x]+w[i]){
                dis[y]=dis[x]+w[i];
                q.push(y);
            }
        }
    }
}
int main(){
    cin>>n>>m;
    while(m--){
        int x,y,z;cin>>x>>y>>z;
        add(x,y,z);
    }
    spfa();
    cout<<dis[n];
    return 0;
}
复制代码

 SPFA判断负环

思路:在spfa的基础上新建要给cnt数组,统计各点入队次数。若入队次数>=n则一定存在负权换。

复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=6e3+5;
int n,m,s;
int head[N],ne[N],ver[N],w[N],tot;
void add(int x,int y,int z){//邻接表存边,也是图的通常存储应用 
    ver[++tot]=y,w[tot] =z;
    ne[tot]=head[x],head[x]=tot;
}

int dis[N];//记录点i到起点的最短距离 
int cnt[N];//记录点i的入队次数 
queue<int> q;
bool spfa(){
    memset(dis,0x3f,sizeof dis);//初始化其他点为正无穷 
    dis[1]=0;q.push(1);
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=head[x];i;i=ne[i]){
            int y=ver[i];
            if(dis[y]>dis[x]+w[i]){//注意dis[x]+w[i]在有些题目当中出现爆int情况 
                cnt[y]=cnt[x]+1;
                if(cnt[y]>=n) return true;//入队超过n-1次,存在负权环 
                dis[y]=dis[x]+w[i];
                q.push(y);
            }
        }
    }
    return false;
}
void solve(){
    for(int i=0;i<=6000;i++){//多测清空 
        head[i]=ne[i]=ver[i]=w[i]=cnt[i]=tot=0;
        queue<int> empty;
        swap(empty,q);
    }
    cin>>n>>m;
    while(m--){
        int x,y,z;cin>>x>>y>>z;
        add(x,y,z);
        if(z>=0) add(y,x,z);
    }
    if(spfa()) cout<<"YES"<<endl;
    else cout<<"NO"<<endl;
}
int main(){
    int T;cin>>T;
    while(T--)    solve();
    return 0;
}
复制代码

 

 三、dijkstra最短路算法

  1)spfa算法复杂度不可控在于每个点可能多次入队出队,若能保证每个点只出队一次,那么算法将会更加稳定。dijkstra则是这样的思想对spfa进行优化。

  2)若存在一个全为非负边权的有向图,我们初始化起点dis[s]=0,其他点为正无穷。按照各点到起点的距离从小到大排序依次出队,当某点x要执行出队操作时,此时dis[x]在队列中一定最小,即不存在一个路径s->k->x,使得dis[k]+w[k,x]<dis[x]。因为dis[k]>dis[x],那么只要k到x的边权为正,则dis[x]就一定为最短路径。这也是dijkstra不能处理负边权的原因。

  3)通常有优先队列priority_queue进行处理。每个点出队一次,加上优先队列内部排序,总体复杂度约为:O(m*logn)

 

 

 dijkstra处理最短路

复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,s;
int head[N],ne[N<<1],ver[N<<1],w[N<<1],tot;
void add(int x,int y,int z){//邻接表存边,也是图的通常存储应用 
    ver[++tot]=y,w[tot] =z;
    ne[tot]=head[x],head[x]=tot;
}

struct node{
    int id,d;
    bool operator < (const node &p) const{//按到起点的距离从小到大排序 
        return d>p.d;
    }    
};
bool vis[N];//记录点i的入队情况 
int dis[N];//记录到起点的最短距离 
priority_queue<node> q;
void dijkstra(){
    memset(dis,0x3f,sizeof dis);
    q.push({s,0});dis[s]=0;//初始化起点距离为0,其他点距离为正无穷  
    while(!q.empty()){
        node tmp=q.top();q.pop();
        int x=tmp.id;
        if(vis[x]) continue;//标记出队,出队时当前距离一定最短 
        vis[x]=true;
        for(int i=head[x];i;i=ne[i]){
            int y=ver[i];
            dis[y]=min(dis[y],dis[x]+w[i]);
            q.push({y,dis[y]});
        }
    } 
}
int main(){
    cin>>n>>m>>s;
    while(m--){
        int x,y,z;cin>>x>>y>>z;
        add(x,y,z);
    }
    dijkstra();
    for(int i=1;i<=n;i++) cout<<dis[i]<<" ";
    return 0;
}
复制代码

 四、bellman-ford算法

  1)基本思想:跟spfa和dijsktra一样,每个顶点进行距离更新时都是由某一条边决定。即dis[y] > dis[x]+w时进行更新(通过x->y这条边更新y到顶点得距离)。我们初始化起点距离为0,其他点为正无穷。显然,我们遍历m条边,通过对每条边进行松弛操作(通过该边更新y的距离),一次完整得遍历至少会有一个点的距离产生更新。那么对于n个顶点,由于起点已经有了最小值,只需要进行n-1次遍历边进行松弛即可。复杂度为O(n*m)

  2)优化:若在一次遍历边的过程中,若没有产生新的距离更新,则此时所有点的最短距离已经固定,可以提前结束。

  3)判断负环:执行完n-1次遍历边的操作后,此时已经得出答案。如果在第n次再次执行遍历边操作依然产生了新的更新,说明存在负环。

  4)常见应用:对于某条路径s->t,执行一次松弛操作等价于最多为路径添加一条边。所以bellman-ford可以处理有边数限制的最短路。

Bellman_ford核心代码

复制代码
struct node{ int x,y,w; } a[N];
int dis[N]; 
bool bellman_ford(){//核心代码 
    memset(dis,0x3f,sizeof dis);//其他边距离初始化为正无穷 
    dis[1]=0;//起点为0 
    for(int k=1;k<n;k++){//n-1轮遍历边的操作 
        bool flag=false; 
        for(int i=1;i<=m;i++){//逐条边遍历 
            int x=a[i].x,y=a[i].y,w=a[i].w;
            if(dis[y]>dis[x]+w && dis[x]!=0x3f3f3f3f){//正无穷过来的边不用更新 
                dis[y]=dis[x]+w;
                flag=true;
            }
        }
        if(!flag) break;//没有再产生更新,提前结束。 
    }
    for(int i=1;i<=m;i++){//第n次松弛,若还存在更新,则说明存在负环 
        int x=a[i].x,y=a[i].y,w=a[i].w;
        if(dis[y]>dis[x]+w && dis[x]!=0x3f3f3f3f) return true; 
    }
    return false;
} 
复制代码

 

最短路问题扩展技巧

1.最短路径输出方案 例题:T317647

开一个 pre 数组,在更新距离的时候记录下来后面的点是如何转移过去的,算法结束前再递归地输出路径即可。

比如 Floyd 就要记录 pre[i][j] = k;SPFA 和 Dijkstra 一般记录 pre[v] = u

 

2.传递闭包(连通性的判断)例题:T319398

利用floyd算法原理,将i,j两点的连通性通过中间点k进行转移。如果 (i,k) && (k,j) ,那么(i,j)就连通。另外不要忽略(i,j)原本就联通的情况。

 

3.有边数限制的最短路 例题:T317715

bellman_ford通过对边进行松弛,松弛k次的时候,此时最短路径所用的边数一定是小于等于k,利用这个性质可以进行问题的求解。

 

算法总结对比

算法/对比 主要使用方向 时间复杂度 处理负边权 处理负权回路
Floyd 带权图的多源最短路径 O(m3) YES NO
SPFA 带权图的单源最短路径 最坏O(n*m) YES YES
dijkstra 带权图的单源最短路径 约为O(m*logn) NO NO
Bell-man Ford 带权图中指定边数的单源最短路 O(n*m) YES   YES

 

posted @   7char  阅读(355)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示