【图论之最短路问题】简单易懂入门篇:Bellman-Ford、Dijkstra和Floyd算法

最短路:从一个点到另一个点的最短距离(边权和最小)
经典的最短路问题大概这几种算法:
最短路问题

一、前缀知识

  • 图的基本概念、有向图无向图、DAG、图的表示(邻接矩阵、邻接表、链式前向星
  • 单源最短路问题:从一个点出发到其他能到达的任意点的最短路径

二、Bellman-Ford算法

  • 适用范围:没有负圈存在的图寻找单源最短路,可以用这个算法来检验是否有负圈
  • 算法复杂度:O(VE) V为点数,E为边数
  • 原理:这个算法给我一种瞎搞的感觉。。。
    大致分为几个步骤:
    1.开一个数组dist[V]用于存各个点到出发点的最短距离,赋初值INF(出发点为0)
    2.每一次都循坏一遍所有的边,若dist[终点]>dist[起点]+边权,则更新dist[终点],这样可以保证每一次的更新都能使dist[终点]变小,上一次循环的结果又可以保证这一次这个点已经到达过了
    3.进行多次循环,直到不再更新,说明所有的dist[]都已经被更新到最短
  • 单看原理比较难以理解,以下面的图举个例子,大致模拟一下算法的流程
    求编号为1的点到所有点的最短路径
    图1
    第一步:初始化dist[v]数组,所有的dist初始化为INF(dist[1]为0)
    (一般INF取0x3f3f3f3f)
    第二步:不断循环更新dist数组
    第一次循环
    第二次循环
    然后我们就发现无法再更新了,这个时候跳出循环就好啦~
    然后就完美地得到了初始点到每个点的最短路径长度了,如果是INF则说明无法到达
  • 伪代码(这里的edge仅仅用一个结构体把每条边存下来而已)
struct edge{int from,to,cost};
edge edg[maxe];
void bellman()
{
  /*V为点数,E为边数,start为出发点*/
  fill(dist,dist+maxv,INF);
  dist[start]=0;
  while(1)
  {
    bool if_update=false;//记录此次循环是否更新
    for(int i=0;i<E;i++)
    {
      edge e=edg[i];
      if(dist[e.from]!=INF&&dist[e.from]+e.cost<dist[e.to])//若该边的起始点已经被路过且满足dist[起始]+cost<dist[终点],则更新
      {
        if_update=true;
        dist[e.to]=dist[e.from]+e.cost;
      }
    }
    if(!up_date) break;
  }
}
  • bellman算法虽然不能用来判断含有负圈的图(?:试想一下如果有负圈,那岂不是转了一圈又一圈停不下来了),但是可以通过这个性质来判断图中有没有负圈:
    我们假设一种最坏情况,每一次的循环只更新一个值的时候(如图)
    最坏情况
    每次循环的时候只更新一个dist,这样整个过程的循环只进行V-1次,所以,只要不存在负圈,整个过程最多进行V-1次循环,我们可以在while里面加个计数器,这样就可以用来判断是否含有负圈了
  • 例题传送门:虫洞问题

三、Dijkstra算法

  • Bellman算法感觉有点浪费时间,就像上面最坏情况那样,总共五个点,需要进行四次循环,每次循环里面都需要遍历一遍所有的边。。。但每次循环实际上却只更新了一次,所以,在Bellman算法的基础上进行了一下优化,就有了Dijkstra算法。然鹅在Dijkstra被优化之前他们时间复杂度是一样的,只不过Dijkstra算法看起来没有那么暴力了。。。。
  • 适用情况:没有负边存在
  • 时间复杂度O(V2
  • 原理:在Bellman算法的基础上,每一次更新的时候都会找一下当前情况下没有被用过并且dist最小的点,对于当前情况下用这个dist来更新其他的dist(这真的有一种dp的感觉,而且满足了无后效性)
    还是上面那个例子,用Dijkstra算法循环的过程如图:
    第一次循环
    第二次循环
    第三次循环
    然后后面就是用dist[4]和dist[5]更新了,但是很明显他们没有边可以走下去了,同一个例子可以发现,Dijkstra算法每个点只用了一次,看起来比Bellman算法要少跑一点然鹅在堆优化之前时间复杂度并没有什么区别。。。
  • 伪代码(用邻接表表示图)
struct edge{int to,cost};
vector<edge> e[maxv];
bool vis[maxv];//vis数组用于记录每个点是否被用来做过一次循环
void Dijkstra()
{
  /*V为点数,E为边数,start为出发点*/
  fill(dist,dist+V,INF);
  fill(vis,vis+V,false);
  dist[start]=0;
  while(1)
  {
    int v=-1;
    for(int u=0;u<V;u++)
      if(!vis[u]&&(v==-1||dist[u]<dist[v])) v=u;//找到没有被用过并且最小的dist并记录下来
    if(v==-1) break;  //所有点都被遍历一遍以后就跳出循环
    vis[v]=true;
    for(int i=0;i<e[v].size();i++)
     dist[e[v][i].to]=min(dist[e[v]+e[v][i].cost, dist[e[v][i].to]);//每次都更新一下dist[v]所能到达的点
  }
}
  • 但是这个算法不能用在存在负边的情况下,试想一下,如果存在一条负边,使得经过了这条边dist就会减小,那dp的无后效性原则就不满足了,Dijkstra算法本来就是在没有负边的情况下可以保证下一次的dist会越来越大而不影响前面已经得到的结果,无负边的条件被破坏掉了,相当于Dijkstra的前提也被破坏掉了
  • 例题传送门:最短路模板题(无需优化)

四、Dijkstra算法的堆优化

  • Dijkstra算法之所以最后的时间复杂度跟Bellman差不多,主要是找到最小边的这个过程的遍历使得时间复杂度变大,所以我们只需要对找到最小值这个过程进行优化,整个算法的时间复杂度就会被优化下来,这里我们用一下c++里的set容器来实现logn的查找,当然用priority_queue也可
  • 时间复杂度O(ElogV)
  • 先强推一下链式前向星,具体就不展开讲了,代码大致是这样子的,想一想应该就能想通,我把代码挂上来
struct edge{
   int to,w,next;
}e[maxe];
int cnt=0;
int h[maxv];
void add(int i,int j,int w)//加边操作
{
    e[++cnt].to=j;
    e[cnt].w=w;
    e[cnt].next=h[i];
    h[i]=cnt;
}

堆优化代码

void dijkstra()
{
    set<P>heap;
    fill(dist,dist+1+n,INF);
    dist[start]=0;
    heap.insert(P(0,u));
    while(!heap.empty())
    {
        int d=heap.begin()->first;
        int v=heap.begin()->second;
        heap.erase(heap.begin());
        if(d>dist[v]) continue;
        for(int i=h[v];i;i=e[i].next)
        {
            edge edg=e[i];
            if(dist[edg.to]>dist[v]+edg.w)
            {
                dist[edg.to]=dist[v]+edg.w;
                heap.insert(P(dist[edg.to],edg.to));
            }
        }
    }
}

五、Floyd算法

  • 时间复杂度:O(V3
  • 该算法可以在O(V3)的时间复杂度下时间找到所有两点间的最短距离,而且实现起来较为简单,并且可以用于存在负边的情况
  • 因为代码比较简单而且用dp的思路就很容易理解了,所以我就不详细展开讲了,直接上代码(邻接矩阵存图)
int dist[maxv][maxv];//数组dist用于存边上的权值,不存在为INF,dist[i][i]=0
void floyd()
{
  for(int k=0;k<V;k++)//V为点数
   for(int i=0;i<V;i++)
    for(int j=0;j<V;j++) 
     dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j]);
}

例题传送门:畅通工程

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值