最短路径问题

最短路径问题

1.最短路径介绍

最短路径问题是图论研究中的一个经典算法问题, 旨在寻找图(由结点和路径组成的)中两结点之间的最短路径。
算法具体的形式包括:
①确定起点的最短路径问题即已知起始结点,求最短路径的问题。
②确定终点的最短路径问题与确定起点的问题相反,该问题是已知终点结点,求最短路径的问题。
在无向图中该问题与确定起点的问题完全等同,
在有向图中该问题等同于把所有路径方向反转的确定起点的问题。
③确定起点终点的最短路径问题即已知起点和终点,求两结点之间的最短路径。
④全局最短路径问题求图中所有的最短路径。

在这里插入图片描述

用于解决最短路径问题的算法被称做“最短路径算法”, 有时被简称作“路径算法”。

最常用的路径算法有:
Bellman-Ford算法
SPFA算法
Dijkstra算法
Floyd-Warshall算法

2、常见算法

①Dijkstra算法

是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。迪杰斯特拉算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止。

基本思想是:
将顶点集合V分成两个集合,一-类是生长点的集合S,包括源点和已经确定最短路径的顶点;另一类是非生长点的集合V(s), 包括所有尚未确定最短路径的顶点,并使用一个待定路径表,存储当前从源点v到每个非生长点vi的最短路径。初始时,S只包含源点v,对v∈V(s),待定路径表为从源点v到vi的有向边。然后在待定路径表中找到当前最短路径v,…
Vk, 将v加入集合S中,对vi∈V(s),将路径v, … vk, vi与待定路径表中从源点v到vi的最短路径相比较,取路径长度较小者为当前最短路径。重复上述过程,直到集合V中全部顶点加入到集合S中。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述对图6-20(b)所示有向网执行Dijkstra 算法,求得从顶点vo到其余各顶点的最短路径,以及算法执行过程中数组dist 和path的变化状况,如表6-3所示。

在这里插入图片描述
代码:

#include<stdio.h>
#include<stdlib.h>
#define max1 10000000  //原词条这里的值太大,导致溢出,后面比较大小时会出错
int a[1000][1000];
int d[1000];//d表示源节点到该节点的最小距离
int p[1000];//p标记访问过的节点
int i, j, k;
int m;//m代表边数
int n;//n代表点数
int main()
{
    scanf("%d%d",&n,&m);
    int    min1;
    int    x,y,z;
    for(i=1;i<=m;i++)
    {
        scanf("%d%d%d",&x,&y,&z);
        a[x][y]=z;
        a[y][x]=z;
    }
    for( i=1; i<=n; i++)
        d[i]=max1;
    d[1]=0;
    for(i=1;i<=n;i++)
    {
        min1 = max1;
        //下面这个for循环的功能类似冒泡排序,目的是找到未访问节点中d[j]值最小的那个节点,
        //作为下一个访问节点,用k标记
        for(j=1;j<=n;j++)
            if(!p[j]&&d[j]<min1)
            {
                min1=d[j];
                k=j;
            }
        //p[k]=d[k]; // 这是原来的代码,用下一 条代码替代。初始时,执行到这里k=1,而d[1]=0
       //从而p[1]等于0,这样的话,上面的循环在之后的每次执行之后,k还是等于1。
        p[k] = 1; //置1表示第k个节点已经访问过了
        for(j=1;j<=n;j++)
            if(a[k][j]!=0&&!p[j]&&d[j]>d[k]+a[k][j])
                d[j]=d[k]+a[k][j];
    }
    //最终输出从源节点到其他每个节点的最小距离
    for(i=1;i<n;i++)
        printf("%d->",d[i]);
    printf("%d\n",d[n]); 
    return 0;
}

堆优化的迪杰斯特拉算法

#include<bits/stdc++.h>
#define M(x,y) make_pair(x,y)
using namespace std;
int fr[100010],to[200010],nex[200010],v[200010],tl,d[100010];
bool b[100010];
void add(int x,int y,int w){
    to[++tl]=y;
    v[tl]=w;
    nex[tl]=fr[x];
    fr[x]=tl;
}
priority_queue< pair<int,int> > q;
int main(){
    int n,m,x,y,z,s;
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);
    }
    for(int i=1;i<=n;i++) d[i]=1e10;
    d[s]=0;
    q.push(M(0,s));
    while(!q.empty()){
        int x=q.top().second;
        q.pop(); 
        if(b[x]) continue;
        b[x]=1;
        for(int i=fr[x];i;i=nex[i]){
            int y=to[i],l=v[i];
            if(d[y]>d[x]+l){
                d[y]=d[x]+l;
                q.push(M(-d[y],y));//懒得重载运算符
            }
        }
    }
    for(int i=1;i<=n;i++) printf("%d ",d[i]);
    return 0;
}

②Bellman-Ford算法

贝尔曼-福特算法(Bellman-Ford)是求解单源最短路径问题的一种算法。它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。其优于迪科斯彻算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达O(VE)。但算法可以进行若干种优化,提高了效率。

Floyd算法用于求每一对顶点之间的最短路径问题,问题描述如下:给定带权有向图G=(V, E),对任意顶点vi和vj (i≠j) ,求顶点Vi到顶点Vj的最短路径。

Floyd算法的基本思想是:
假设从v;到v;的弧(若从v;到v;的弧不存在,则将其弧的
权值看成∞)是最短路径,然后进行n次试探。首先比较vi, Vj和vi; vo, v;的路径长度,取长度较短者作为从vi到vj中间顶点的编号不大于0的最短路径。在路径上再增加一个顶点vI,将vi,…vI,…vj和已经得到的从vi到vj中间顶点的编号不大于0的最短路径相比较,取长度较短者作为中间顶点的编号不大于1的最短路径。
依此类推,在一般情况下,若vi,…,vk和Vk,…vj分别是从vi到vk和从Vk到vj中间顶点的编号不大于k-1的最短路径,则将vi,…,vk,…,vj和已经得到的从vi到vj中间顶点的编号不大于k-1的最短路径相比较,取长度较短者为从vi到vj中间顶点的编号不大于k的最短路径。经过n次比较后,最后求得的必是从Vi到vj的最短路径。
在这里插入图片描述
伪代码表示

procedure BellmanFord(list vertices, list edges, vertex source)
   // 该实现读入边和节点的列表,并向两个数组(distance和predecessor)中写入最短路径信息
 
   // 步骤1:初始化图
   for each vertex v in vertices:
       if v is source then distance[v] := 0
       else distance[v] := infinity
       predecessor[v] := null
 
   // 步骤2:重复对每一条边进行松弛操作
   for i from 1 to size(vertices)-1:
       for each edge (u, v) with weight w in edges:
           if distance[u] + w < distance[v]:
               distance[v] := distance[u] + w
               predecessor[v] := u
 
   // 步骤3:检查负权环
   for each edge (u, v) with weight w in edges:
       if distance[u] + w < distance[v]:
           error "图包含了负权环"

③*SPFA算法

SPFA(最短路径快速算法,英语:Shortest Path Faster Algorithm)是一个用于求解有向带权图单源最短路径的改良的贝尔曼-福特算法。这一算法被认为在随机的稀疏图上表现出色,并且极其适合带有负边权的图。然而SPFA在最坏情况的时间复杂度与贝尔曼-福特算法相同,因此在非负边权的图中仍然最好使用迪杰斯特拉算法(Dijkstra)。

SPFA算法的基本思路与贝尔曼-福特算法相同,即每个节点都被用作用于松弛其相邻节点的备选节点。相较于贝尔曼-福特算法,SPFA算法的提升在于它并不盲目尝试所有节点,而是维护一个备选节点队列,并且仅有节点被松弛后才会放入队列中。整个流程不断重复直至没有节点可以被松弛。

下面我们采用SPFA算法对下图求v1到各个顶点的最短路径:
在这里插入图片描述
初始化:

首先我们先初始化数组dis如下图所示:(除了起点赋值为0外,其他顶点的对应的dis的值都赋予无穷大,这样有利于后续的松弛)
在这里插入图片描述
此时,我们还要把v1如队列:{v1}
现在进入循环,直到队列为空才退出循环。

第一次循环:

首先,队首元素出队列,即是v1出队列,然后,对以v1为弧尾的边对应的弧头顶点进行松弛操作,可以发现v1到v3,v5,v6三个顶点的最短路径变短了,更新dis数组的值,得到如下结果:
在这里插入图片描述我们发现v3,v5,v6都被松弛了,而且不在队列中,所以要他们都加入到队列中:{v3,v5,v6}

第二次循环

此时,队首元素为v3,v3出队列,然后,对以v3为弧尾的边对应的弧头顶点进行松弛操作,可以发现v1到v4的边,经过v3松弛变短了,所以更新dis数组,得到如下结果:

在这里插入图片描述此时只有v4对应的值被更新了,而且v4不在队列中,则把它加入到队列中:{v5,v6,v4}

第三次循环

此时,队首元素为v5,v5出队列,然后,对以v5为弧尾的边对应的弧头顶点进行松弛操作,发现v1到v4和v6的最短路径,经过v5的松弛都变短了,更新dis的数组,得到如下结果:

在这里插入图片描述我们发现v4、v6对应的值都被更新了,但是他们都在队列中了,所以不用对队列做任何操作。队列值为:{v6,v4}

第四次循环

此时,队首元素为v6,v6出队列,然后,对以v6为弧尾的边对应的弧头顶点进行松弛操作,发现v6出度为0,所以我们不用对dis数组做任何操作,其结果和上图一样,队列同样不用做任何操作,它的值为:{v4}

第五次循环

此时,队首元素为v4,v4出队列,然后,对以v4为弧尾的边对应的弧头顶点进行松弛操作,可以发现v1到v6的最短路径,经过v4松弛变短了,所以更新dis数组,得到如下结果:

在这里插入图片描述因为我修改了v6对应的值,而且v6也不在队列中,所以我们把v6加入队列,{v6}

第六次循环

此时,队首元素为v6,v6出队列,然后,对以v6为弧尾的边对应的弧头顶点进行松弛操作,发现v6出度为0,所以我们不用对dis数组做任何操作,其结果和上图一样,队列同样不用做任何操作。所以此时队列为空。

OK,队列循环结果,此时我们也得到了v1到各个顶点的最短路径的值了,它就是dis数组各个顶点对应的值,如下图:

在这里插入图片描述

int SPFA(int s) {
      queue<int> q;
      bool inq[maxn] = {false};
      for(int i = 1; i <= N; i++) dis[i] = 2147483647;
      dis[s] = 0;
      q.push(s); inq[s] = true;
      while(!q.empty()) {
          int x = q.front(); q.pop();
          inq[x] = false;
         for(int i = front[x]; i !=0 ; i = e[i].next) {
             int k = e[i].v;
             if(dis[k] > dis[x] + e[i].w) {
                 dis[k] = dis[x] + e[i].w;
                 if(!inq[k]) {
                     inq[k] = true;
                     q.push(k);
                 }
             }
         }
     }
     for(int i =  1; i <= N; i++) cout << dis[i] << ' ';
     cout << endl;
     return 0;
 }

松弛

每次松弛操作实际上是对相邻节点的访问,第n次松弛操作保证了所有深度为n的路径最短。由于图的最短路径最长不会经过超过|V|-1条边,所以可知贝尔曼-福特算法所得为最短路径。

负边权操作

与迪杰斯特拉算法不同的是,迪杰斯特拉算法的基本操作“拓展”是在深度上寻路,而“松弛”操作则是在广度上寻路,这就确定了贝尔曼-福特算法可以对负边进行操作而不会影响结果。
正权图使用dijkstra算法,负权图使用SPFA算法

负权环判定

因为负权环可以无限制的降低总花费,所以如果发现第
次操作仍可降低花销,就一定存在负权环。

④Floyd - Warshall(弗洛伊德算法)

模板模板
视频资源:
https://www.bilibili.com/video/BV1LE411R7CS?from=search&seid=1868101711902580696

参考博客:
https://blog.csdn.net/qq_35644234/article/details/61614581

https://blog.csdn.net/sugarbliss/article/details/86511043?ops_request_misc=%25257B%252522request%25255Fid%252522%25253A%252522161043542716780255270397%252522%25252C%252522scm%252522%25253A%25252220140713.130102334…%252522%25257D&request_id=161043542716780255270397&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_click~default-1-86511043.first_rank_v2_pc_rank_v29&utm_term=%E6%9C%80%E7%9F%AD%E8%B7%AF

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值