最短路问题(Dijkstra、Bellman-Ford、spfa、Floyd)

目录

目录

1.单源最短路

1.1所有权边都是正数

1.1.1朴素Dijsktra算法

1.1.2  堆优化Dijkstra算法

1.2存在负权边的最短路径问题

1.2.1 Bellman-Ford算法

1.2.2 SPFA(Bellman-Ford优化)

1.2.3 SPFA判断负权环

2.多源最短路问题

Floyd算法



1.单源最短路

1.1所有权边都是正数

1.1.1朴素Dijsktra算法

a.算法描述

(1)初始化dist数组,dist[1]=0,dist[∞]。集合S={已经求得最短路径的结点},集合V={还未求得最短路径的结点}。

(2)设当前检索的结点下标为i,对于结点j∈V,若(i,j)存在且dist[i]+(i,j)<dist[j]的值,则更新dist[j]。

(3)在全部更新结束后,从集合V里选取dist值最小的结点作为下一个扩展结点,并将其加入集合S。

(4)若此时结点已经全部遍历完,算法结束,否则重复执行步骤(2).

b.代码实现

#include<iostream>
#include<cstring>
#include<math.h>
using namespace std;
const int N=510;
int g[N][N];//邻接矩阵
int dist[N];//the minimum distance of each point
bool str[N];//在集合S还是在集合V里面的标志,str=true代表已经求得最短路径,false代表还在集合V中
int n,m;
void dijkstra(){
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;//初始化
    int count=n-1;
    //注意是n-1此循环,在扩展到n-1个结点之后,求得的dist[n]的值就是结点1到结点n的最短路径
    while(count--){//n-1次循环,找最小dist[]→更新dist[]
    //开始寻找dist[j]最小的结点
        int t=-1;
        for(int i=1;i<=n;i++){
            if(!str[i] && (t==-1 || dist[i]<dist[t])){//如果还没有求得最短路径并且是当前dist最小的结点
                t=i;
            }
        }
        str[t]=true;//加入集合S中
        for(int i=1;i<=n;i++){
            dist[i]=min(dist[t]+g[t][i],dist[i]);//更新dist的值
        }
    }
    if(dist[n]==0x3f3f3f3f) cout<<-1;
    else cout<<dist[n];
}
int main(){
    cin>>n>>m;
    memset(g,0x3f,sizeof g);
    while(m--){
        int a,b,c;
        cin>>a>>b>>c;
        if(a!=b) g[a][b]=min(g[a][b],c);//由于重边与自环的存在,自环可以忽略,重边需要取最短
    }
    dijsktra();
}

 c.时间复杂度分析

找到当前dist值最小的结点需要遍历n次,而我们需要进行n次这样的过程,朴素dijstra算法时间复杂度为O(n²)。

1.1.2  堆优化Dijkstra算法

a.算法描述

朴素的dijkstra算法主要在寻找dist最小的结点上耗费时间,而堆取得最小值只需要O(1)的时间复杂度。

b.代码实现

采用模拟堆的方式,堆存放的内容为二元组pair<int,int>,first表示下标,second表示对应的dist的值,根据dist的值来排序。

1.定义向上调整函数up:

只要堆的序号x满足:不是根结点、父亲结点的值更大,就交换heap[x]与heap[x/2]的内容。

void up(int x) {
    while(x / 2 && heap[x / 2].second > heap[x].second){
        swap(heap[x/2],heap[x]);
        x=x/2;
    }
}

2.定义向下调整函数down:

用t来记录当前需要堆调整的序号,检索heap[2*t].second和heap[2*t+1].second的内容,完成交换。如果此时x=t,说明已经调整完毕;如果此时x!=t,当前需要调整的位置为t,在调用down(t)函数。

void down(int x) {
	int t = x;
	if (2 * x<= Size && heap[2 * x].second < heap[t].second) t = 2 * x;
	if (2 * x + 1 <= Size && heap[2 * x + 1].second < heap[t].second) t = 2 * x + 1;
	swap(heap[t], heap[x]);
	if (t != x)down(t);
}

 3.dijkstra()

void dijkstra(){
    heap[++Size]={1,0};//将1号结点插入优先队列中
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    int count=n-1;
    while(count--){
        auto t=heap[1];
//删除堆顶元素:用最后一个元素覆盖堆顶元素,然后向下调整
        swap(heap[1],heap[Size]);
        Size--;
        down(1);
        if(!str[t.first]){//如果是在集合V中,没有被遍历过,因为dist的值会被更新很多次,有可能会取到同一个点
            //放入集合S中
            str[t.first]=true;
            int point=Head[t.first];
            while(point!=-1){//该结点所有的扩展结点,更新dist值
                int j=e[point];
                int distance=w[point];
                if(!str[j] && t.second+distance<dist[j]){
                    dist[j]=t.second+distance;
//插入元素,在堆的末尾插入元素,再向上调整
                    heap[++Size]={j,dist[j]};
                    up(Size);
                }
                point=Next[point];
            } 
        }
    }
    if(dist[n]==0x3f3f3f3f) cout<<-1;
    else cout<<dist[n];
}

c.时间复杂度分析

堆中查找一个元素只需要O(1)的时间复杂度,若维护堆的结点数为n,即更新dist值时是修改堆中的值,堆查找的时间复杂度是O(logn),有m条边,那么时间复杂度是O(mlogn)。

1.2存在负权边的最短路径问题

1.2.1 Bellman-Ford算法

a.算法描述

Bellman-Ford算法的策略是对于有n个结点且有负权边的图,进行n次循环,每一次循环,若结点a到结点b存在一条边,进行一次更新:dist[b]=min{dist[a]+w(a,b),dist[b]}.

Bellman-Ford算法类似与广度优先搜索策略。在一个含有n个结点的图中,任意两点之间的最短路径最多只包括n-1条边。进行第一次更新,可以得到源点经过一条边到达其他结点的最短距离;进行第二次更新,可以得到源点经过两条边到达其他结点的最短距离……因而,至多进行n次循环,可以求出源点到所有其他结点的最短距离。

b.代码实现

在实现Bellman-Ford算法时,需要有以下几方面要注意:

1.当图中有负权回路时,最短路径有可能不存在。这是因为从源点到结点可以经过无限次这个负权回路,那么dist值会不断减小。当然,若这个负权回路不在任何到达这个结点的路径上,最短路径依然存在。而判断是否存在负权回路,只需要进行n次松弛操作(dist[b]=min{dist[a]+w(a,b),dist[b]}称为一次松弛),若此时仍然有结点的dist值进行了更新,那么就存在负权回路。这是因为对于一个只有n个结点的图,源点到该结点经过了n条路径,那么该路径上一定存在回路,而dist值又减小了,说明经过的该回路一定时负权回路。下面的代码求的是经过k条路径,即有边数限制的最短路径,那有负权回路也不会影响。

2.在每一次循环前,需要对dist数组进行备份。这是因为在第i次循环的时候,我们是在第i-1次循环后得到的dist数组的基础上修改的。如果直接使用dist数组,会有可能用到本次循环前面的结果,那相当于第i次循环,得到了源点经过i+1条边到达该结点的最短距离。

3.在初始化dist数组时,首先是要提前修改dist[1]为0。另外,对于最短路径不存在的情况,不能再使用判定条件:if(dist[n]>0x3f3f3f3f),这是因为尽管源点到结点n是不可达的,但是有可能存在同样不可达的结点到结点n之间有负边,此时dist[n]会比0x3f3f3f3f小.

#include<iostream>
#include<cstring>
#include<math.h>
using namespace std;
const int N=510;
const int M=10010;
struct Edge{
    int a;
    int b;
    int w;
}edges[M];//定义结构体Edge,存储图的结构
int n,m,k,Index;
int dist[N];//距离
int backup[N];
void Bellman_Ford(){
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    for(int i=1;i<=k;i++){
        memcpy(backup, dist, sizeof dist);//复制,将dist的所有内容复制到backup中
        //warning1:而要复制的原因是,在遍历的过程中,有可能用到了本次遍历前面的更新结果,是不允许的
        for(int j=0;j<m;j++){
            dist[edges[j].b]=min(backup[edges[j].a]+edges[j].w,dist[edges[j].b]);//松弛
        }
    }
    if(dist[n]>0x3f3f3f3f/2) cout<<"impossible";
    //warning2:尽管源点到结点n是不可达的,但是存在同样不可达的结点到结点n之间有负边,此时
    //dist[n]不再是0x3f3f3f3f
    else cout<<dist[n];
    //warning3:如果有负权回路的存在,可以无限次经过这个回路,会没有最短距离
    //但是本题有限制经过k条边,因而有负权边也没有关系
}
int main(){
    cin>>n>>m>>k;
    for(int i=1;i<=m;i++){
        int a,b,w;
        cin>>a>>b>>w;
        edges[Index++]={a,b,w};
    }
    Bellman_Ford();
}

c.时间复杂度分析

Bellman-Ford算法所耗费的时间主要来源于两层循环,进行n次循环,每次循环检索n条边,因而时间复杂度为O(mn).

1.2.2 SPFA(Bellman-Ford优化)

a.算法描述

在Bellman-Ford算法中,松弛操作dist[b]=min{dist[a]+w(a,b),dist[b]}.只有在dist[a]发生了变化的时候,松弛操作才有意义。因而SPFA算法即是在该方面对Bellman-Ford算法做优化:定义一个队列,只有当某个结点的dist值发生变化时,才将其加入队列,每次取队列头元素,找它的扩展结点。

换句话说,Bellman-Ford算法也将dist值没有变化的结点仍然进行了扩展(遍历每条边,事实上对每一个结点都进行了扩展)。SPFA算法使得在进行“广度搜索”时,只将dist值发生了变化的结点进行扩展,dist值没有发生变化的结点是没有意义的,也就是说,本质就是,dist[i]每变化(减小)一次,与其有边相连的结点的dist的值就有可能会发生变化,那么我们就要把它加入到队列中进行扩展(修改)

b.代码实现

SPFA代码实现最主要的就是队列,这里采用数组模拟队列,只不过这里加入队列的判定条件是:不仅你要和它有边相连,还要满足:dist[t]+w[point]<dist[j]

#include<iostream>
#include<math.h>
#include<cstring>
using namespace std;
const int N=100010;
int Head[N],e[2*N],Next[2*N],w[2*N],Index;
int dist[N];
int n,m;
int Queue[N];
int tail,head;
bool str[N];
void add(int a,int b,int c){
    e[Index]=b;
    Next[Index]=Head[a];
    w[Index]=c;
    Head[a]=Index++;
}
void SPFA(){
    while(head<tail){
        int t=Queue[head++];//取出元素,扩展
        int point=Head[t];
        str[t]=false;
        while(point!=-1){
            int j=e[point];
            if(dist[t]+w[point]<dist[j]){
                dist[j]=dist[t]+w[point];
                if(!str[j]){
                    Queue[tail++]=j;
                    str[j]=true;   
                }
                //warning:str数组的作用是,如果队列中已经有结点j了,而现在发现
                //dist[t]+w[point]<dist[j],按理来说dist[j]发生了修改,要加入队列,
                //但是此时可以不必加入队列,在后续要扩展结点j时,dist已经修改了
                //如果此时再加入队列,如果在扩展两个dist[j]之间,dist[j]没有发生变化
                //那相当于重复操作,如果dist[j]发生了变化,我又会把新的dist[j]加入进来,节省时间~
            }
            point=Next[point];
        }
    }
    if(dist[n]==0x3f3f3f3f) cout<<"impossible";
    else cout<<dist[n];
}
int main(){
    memset(Head,-1,sizeof Head);
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    Queue[tail++]=1;
    str[1]=true;
    SPFA();
}

c.时间复杂度分析

一般情况下为O(m),最差为O(mn).(?)

1.2.3 SPFA判断负权环

a.算法描述

在SPFA算法求最短路径的基础上,我们增加一个count数组,可以类似于搜索的深度,即count[j]表示结点1从结点j的最短路径上的边数。每当由结点t使得dist[j]更新,即dist[t]+w(t,j)<dist[j]时,执行操作:count[j]=count[t]+1;

这样,当某个count[j]>=n时,我们就可以判断存在负环。首先,由于图的结点只有n个,当路径长度为n时,说明该条路径一定存在环。而这条路径又是最短路径,说明该环一定是负环(不然我为什么要走这个环)……

b.代码实现

在spfa算法判断负环时要注意以下几点

1.在用spa算法求最短路径时,我们会把头节点先加入到队列中,但spfa算法判断负环在队列初始化时,要把所有的结点都加入到队列中。这是因为图并不一定是连通的,该负环不一定是从源点1出发的负环。因而将所有的结点加入队列后,我们一定可以遍历到环中的某个结点,而整个环是负环,dist数组的更新就会在这个环里面打圈圈~,最后count[j]会大于n;和源点1相连的结点放入队列也没有影响,反正迟早也是要更新的...

2.会存在有情况说,从队列出去的元素再更新dist值时重新回到队列,因为队列的长度肯定不能定义为N..(如果使用数组模拟队列的话),最好是使用循环队列,即head和tail在到达长度时,重新置0;

#include<iostream>
#include<cstring>
#include <algorithm>
using namespace std;
const int N=4000000,M=10010;
int Head[N],e[M],Next[M],w[M],Index;
int Queue[N];//warning1:在手写队列中,出队列只是把head挪后了,因而插入队列总的结点数可能会多于N个
int head,tail;
int dist[N];
bool str[N];
int Count[N];//一旦超过n,就会出现负环
int m,n;
void add(int a,int b,int c){
    e[Index]=b;
    w[Index]=c;
    Next[Index]=Head[a];
    Head[a]=Index++;
}
bool spfa(){
    for(int i=1;i<=n;i++){
        Queue[tail++]=i;
        str[i]=true;
    }
    dist[1]=0;
    while(head<tail){
        int t=Queue[head++];
        if(head==N) head=0;
        str[t]=false;
        int point=Head[t];
        while(point!=-1){
            int j=e[point];
            if(dist[j]>dist[t]+w[point]){
                Count[j]=Count[t]+1;//其实就相当于深度,到结点1经过了多少条边
                dist[j]=dist[t]+w[point];
                if(Count[j]>=n){
                    return true;
                }
                if(!str[j]){
                    Queue[tail++]=j;
                    if(tail==N) tail=0;
                    str[j]=true;
                }
            }
            point=Next[point];
        }
    }
    return false;
}
int main(){
    memset(dist,0x3f,sizeof dist);
    memset(Head,-1,sizeof Head);
    cin>>n>>m;
    while(m--){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    if(spfa()) cout<<"Yes";
    else cout<<"No";
}

2.多源最短路问题

Floyd算法

a.算法描述:

Floyd算法是基于动态规划,基本过程是进行三层循环:

g[i][j]=min(g[i][j],g[i][k]+g[k][j]);

b.代码实现

#include<iostream>
#include<math.h>
using namespace std;
const int N=210;
int g[N][N];//邻接矩阵
int n,m,k;
void Floyd(){
    for(int k=1;k<=n;k++){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                g[i][j]=min(g[i][j],g[i][k]+g[k][j]);
            }
        }
    }
}
int main(){
    cin>>n>>m>>k;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(i==j) g[i][j]=0;//自己到自己肯定不能定义为无穷啊...
            else g[i][j]=0x3f3f3f3f;
        }
    }
    while(m--){
        int a,b,c;
        cin>>a>>b>>c;
        g[a][b]=min(g[a][b],c);//重边
    }
    Floyd();
    while(k--){
        int x,y;
        cin>>x>>y;
        if(g[x][y]>0x3f3f3f3f/2) cout<<"impossible"<<endl;
        else cout<<g[x][y]<<endl;
    }
}

c.时间复杂度为O(n³)


感谢AcWing~

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值