【Algorithms】最短路径及常用算法简介

简述

最短路径问题是图论研究中的一个经典算法问题,旨在寻找图(由结点和路径组成的)中两结点之间的最短路径。

解决图中最短路径问题又分为两种——单源最短路径和多源最短路径,单源最短路径的经典算法包括Dijkstra(戴克斯特拉)算法,Bellman-Ford(贝尔曼-福特)算法以及SPFA(Bellman-Ford算法的改进版本)等,多源最短路径有Floyd-Warshall(弗洛伊德)算法等。

 

Floyd-Warshall(弗洛伊德算法)

弗洛伊德算法的主要思想是通过第三点来缩短两点间的路径,例如顶点a和顶点b之间的路径为dis[a][b],如果想使a,b之间的路径缩短,只有引入第三点k,并通过k点来中转,即a->k->b,才可能缩短a,b之间的路径,那么这个k点是哪个点呢,其实并不唯一,k点也有可能有多个,但可以确定的是,在经过k点中转后,所得到的dis[a][k]+dis[k][b]一定小于中转前的dis[a][b],所以就可以总结为从a点到b点只经过前k个点的最短路径,是不是感觉这句话有点熟悉呢[滑稽],没错,这就是动态规划的思想。

下面给出算法完整代码:

注:Floyd-Warshall算法可以处理带有负权边的图,但不能处理带有“负权回路”的图

#include<bits/stdc++.h>
#define rep(i,a,b) for(i=a;i<=b;i++)
using namespace std;
const int Max=1e3+7;
int dis[Max][Max];
int n,m;
int main()
{
    memset(dis,0x3f,sizeof(dis));
    int i,j,k,a,b,v;
    cin>>n>>m;
    rep(i,1,n)dis[i][i]=0;
    rep(i,1,m){
        cin>>a>>b>>v;
        dis[a][b]=v;
    }
    rep(k,1,n){                                     //Floyd核心代码
        rep(i,1,n){
            rep(j,1,n){
                if(dis[i][k]+dis[k][j]<dis[i][j])   //判断是否满足中转条件
                    dis[i][j]=dis[i][k]+dis[k][j];
            }
        }
    }
    rep(i,1,n){
        rep(j,1,n){
            printf("%5d",dis[i][j]);
        }cout<<endl;
    }
    return 0;
}

 

Dijkstra(戴克斯特拉)算法

戴克斯特拉算法是解决单个点到其余各点最短路径的算法,其主要思想为通过其他边来松弛单个点到其余各点的路程。这里我们还需要定义一个dis数组,表示该点到其余各点的未确定最短距离。然后从dis数组中选取一个最小值(离始点最近的点),将它的距离确定为最短,这里是因为每条边都是正数,那么肯定不可能通过第三个点来使得此时距始点最短的距离更短了,因为始点到其他点的距离都比到这个点的距离要大呀<(^-^)>

然后我们就可以通过这个点的边来对始点到其他点的边进行“松弛”,比如设始点为1号点,刚刚确定为最短路径的点为2号点,然后此时的dis[3]=10,dis[2]=1,e[2][3]=4(始点为2,终点为3的边),这时我们就可以通过e[2][3]这条边来对dis[3]进行松弛,因为dis[3]>dis[2]+e[2][3],所以此时的dis[3]应更新为5。这便是戴克斯特拉算法的主要思想:通过其他边来松弛单个点到其余各点的路程。

这样我们就可以总结出算法的基本步骤了:每次找到离始点最近的一个顶点,通过这个点的边来对始点到其余各点的边进行松弛,最终得到始点到其余各点的最短路径。

具体算法代码如下:

注:这里的Dijkstra无法解决带有负权边及负权回路的图,因为此算法是一种基于贪心策略的算法,每次新拓展一个路径最短的点,更新与其相邻的点的路程。当所有边权为正的时候,由于不会存在一个路程更短的没拓展过的点,所以我们可以保证这个点的路程永远不会再被改变。但是如果出现了负权边,当拓展到负权边的时候会产生更短的路径,就有可能破坏了之前已经确定过的最短路径。

#include<bits/stdc++.h>
#define rep(i,a,b) for(i=a;i<=b;i++)
using namespace std;
const int Max=1e3+7;
const int inf=2147483647;
struct Node
{
    int to,val;
    Node(int _to=0,int _val=0){to=_to;val=_val;}
}node;
vector<vector<Node> > e(Max);   //这里用的二维vector数组做的邻接表来储存的点边信息
int book[Max];
int dis[Max];
int n,m,i,j,a,b,v,u;
int main()
{

    freopen("in.htm","r",stdin);
    cin>>n>>m;
    memset(book,0,sizeof(book));
    memset(dis,0x3f,sizeof(dis));
    dis[1]=0;                
    rep(i,1,m){              
        cin>>a>>b>>v;
        if(a==1)dis[b]=v;                   //这里设始点为 1
        e[a].push_back(Node(b,v));
    }
    book[1]=1;
    rep(i,1,n-1){                           //Dijkstra算法核心代码
        int mins=inf;
        rep(j,2,n){            
            if(book[j]==0&&dis[j]<mins){    //找到目前距始点最近的点u
                mins=dis[j];
                u=j;
            }
        }
        book[u]=1;
        rep(j,0,(int)e[u].size()-1){        //通过点u的边来对始点到其他点的边进行松弛
            if(dis[u]+e[u][j].val<dis[e[u][j].to])
                dis[e[u][j].to]=dis[u]+e[u][j].val;
        }
    }
    rep(i,1,n)cout<<dis[i]<<" ";
    cout<<endl;
    return 0;
}

 

Bellman-Ford(贝尔曼-福特)算法——解决负权边

在以上介绍的两种算法中虽然很好的解决了最短路径问题,但多多少少都有一些缺陷,比如无法解决带有负权回路的图。现在我们介绍的这种Bellman-Ford算法就能够解决负权边问题啦O(∩_∩)O~

Bellman-Ford算法其实操作方法与Dijkstra算法如出一辙,只不过它没有进行最短路径的猜测,而是对每一条边都进行了n-1次松弛,这样就确保了每一条边最后都能到达它的最短路径。

代码如下:

#include<bits/stdc++.h>
#define rep(i,a,b) for(i=a;i<=b;i++)
using namespace std;
const int Max=1e3+7;
const int inf=2147483647;
struct Node
{
    int to,val;
    Node(int _to=0,int _val=0){to=_to;val=_val;}
}node;
vector<vector<Node> > e(Max);
int dis[Max];
int n,m,i,j,a,b,v,u;
int main()
{

    freopen("in.htm","r",stdin);
    cin>>n>>m;
    memset(dis,0x3f,sizeof(dis));
    dis[1]=0;
    rep(i,1,m){
        cin>>a>>b>>v;
        e[a].push_back(Node(b,v));
    }
    rep(u,1,n-1)
        rep(i,1,n)
            rep(j,0,(int)e[i].size()-1)
                if(dis[i]+e[i][j].val<dis[e[i][j].to])
                    dis[e[i][j].to]=dis[i]+e[i][j].val;
    rep(i,1,n)cout<<dis[i]<<" ";
    cout<<endl;
    return 0;
}

有些爱动脑的同学可能会问了,那这样我们会不会做了很多不必要的操作呀,的确,我们在对边进行松弛的过程中,有些边经过少于n-1次的松弛便得到了最短路程,但是我们依旧对它进行了后续没有必要的松弛,这显然是不合适的。所以在这里我们引进了Bellman-Ford算法的队列优化(SPFA)

SPFA(Bellman-Ford算法的队列优化)

在刚刚的算法中我们提到,对每一条边都进行n-1次的松弛,可能会进行很多不必要的操作,浪费掉很多时间,顺便说一下,不进行优化的Bellman-Ford算法的时间复杂度为O(n*m),而经过队列优化的Bellman-Ford算法的时间复杂度最坏情况下也是O(n*m),但是一般是达不到的。

那么我们应该如何利用队列进行优化呢?我们可以将始点u放入队列,然后对u的所有出边进行松弛。例如有一条u->v的边,如果通过u->v这条边使得始点到顶点v的路程变短(dis[u]+e[u][v]<dis[v]),且顶点v不在队列中,就将点v放入队尾,点u松弛完毕后,就将点u出队,继续进行下一点的出边松弛,直到队列为空。

#include<bits/stdc++.h>
#define rep(i,a,b) for(i=a;i<=b;i++)
using namespace std;
const int Max=1e3+7;
const int inf=2147483647;
struct Node
{
    int to,val;
    Node(int _to=0,int _val=0){to=_to;val=_val;}
}node;
vector<vector<Node> > e(Max);
queue<int> q;
int book[Max];          //判断队列中是否已有该点
int dis[Max];
int n,m,i,j,a,b,v,u;
int main()
{

    freopen("in.htm","r",stdin);
    cin>>n>>m;
    memset(dis,0x3f,sizeof(dis));
    memset(book,0,sizeof(book));
    dis[1]=0;
    q.push(1);
    book[1]=1;
    rep(i,1,m){
        cin>>a>>b>>v;
        e[a].push_back(Node(b,v));
    }
    while(!q.empty()){
            i=q.front();
            q.pop();
            rep(j,0,(int)e[i].size()-1)
                if(dis[i]+e[i][j].val<dis[e[i][j].to]){
                    dis[e[i][j].to]=dis[i]+e[i][j].val;
                    q.push(e[i][j].to);
                    book[e[i][j].to]=1;
                }
            book[i]=0;
    }
    rep(i,1,n)cout<<dis[i]<<" ";
    cout<<endl;
    return 0;
}

看到这里爱思考的同学可能会发现了,使用队列优化的Bellman-Ford算法看起来与BFS十分相似,但是不同的是BFS的队列元素出队后一般就不会再入队了,但是这里一个顶点出队后很有可能再入队,因为如果找到了一条到点v的比之前所找到的路程更短的路径,那么也会对经过点v的路径产生影响,所以需要对点v的所有出边再进行一次松弛,这样才能够保证相邻顶点的最短路程同步更新。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值