图算法 单源最短路径 Dijkstra算法(邻接表/邻接矩阵+优先队列STL)

您所在的位置:网站首页 stl优先队列复杂度 图算法 单源最短路径 Dijkstra算法(邻接表/邻接矩阵+优先队列STL)

图算法 单源最短路径 Dijkstra算法(邻接表/邻接矩阵+优先队列STL)

2024-07-08 11:55:17| 来源: 网络整理| 查看: 265

一、前言

  最短路径算法,顾名思义就是求解某点到某点的最短的距离、消耗、费用等等,有各种各样的描述,在地图上看,可以说是图上一个地点到达另外一个地点的最短的距离。比方说,我们把地图上的每一个城市想象成一个点,从一个城市到另一个城市的花费是不一样的。现在我们要从上海去往北京,需要考虑的是找到一条路线,使得从上海到北京的花费最小。有人可能首先会想到,飞机直达啊,这当然是时间消耗最小的方法,但是考虑到费用的高昂,这条线路甚至还不如上海到北京的高铁可取。更有甚者,假设国家开通了从上海到西藏,再从西藏到兰州等等城市经过万般周折最后到达北京的一条线路,虽然要需要经历较长一段时间,但是价钱相比前二者非常实惠(假设只要一块钱,便能跑大半个中国,领略多省风光),单从省钱的角度看来,自然最后这条是可取的。这就是我们在这里所说的单源最短路径。我们接下来的篇幅中将去讲解所有边权值为非负的有向图的单源最短路径,由于无向图相当于变相的有向图,在这里就不做解释,留作读者自行推广。

二、概念

  这里我们讲解最短路径,需要掌握几个基本的概念:

  对于有向图G=(V,E),权值函数W: E→R(即每条边的权值都为一个实数)

  1、路径

      

    表示从v1到vk的一条路径,它的权值为:

      

    例:

    

  2、最短路径:从u到v的一条路径,使w(p)最小,w(p)。

  3、最短路径权值:

    

  注意,最短路径可能不存在:

    (1)存在负权回路,例如:

      

      可以看出,存在v1到v6的负权回路,它的权值为-3,如果我们想找从u到v的最短路径,那么无限循环地走这个负权回路可以使最短路径越来越小,最后达到负无穷,     那么就说明找不到从u到v的最短路径。

    (2)不存在从u到v的路径,这个是肯定不会存在最短路径的。

三、最优子结构

  我们不难发现,求解源点到某一顶点的最短路径,其实不比求解源点到所有顶点的路径简单。这个时候我们要引入全局的概念,能不能找出所有的顶点的最短路径,然后再去查看到目标点的最短路径呢?很多人就会想到动态规划这一思想,说道动态规划,自然我们首先要考虑的问题是最优子结构。

  最短路径满足最优子结构性质:最短路径的子路径是最短路径。

  证明:(剪贴法)

  

    前提:u到v是最短路径。

    假设:x到y不是最短路径,那么存在一条更短的路径从x到y(假设为下面的弯箭头),这样,删去原路径中从x到y的路径,用新找到的路径替代(弯箭头),那么就得到       了一条比u到v的权值更短的路径,这与前提u到v是最短路径相矛盾,因而x到y是最短路径,即最短路径满足最优子结构性质。

  引入三角不等式的概念:(从u到v的最短路径权值,小于等于从u到x的最短路径权值加上从x到v的最短路径权值)

    

    这个性质根据最优子结构性质而来,非常重要。

四、单元最短路径问题

  对于图G= (V, E),给定源点s,找到从s到所有顶点v的最短路径。

  由于在本文中我们讲解Dijkstra算法,需要假设没有负权值,即:

    

  因而,只要路径存在,便存在最短路径。

五、Dijkstra算法思想

  Dijkstra是非常非常著名的计算机科学家,可能很多人对他的了解只在他的单元最短路径算法上,对操作系统了解的人可能还了解他的银行家算法,在方法学领域还有goto有害论等等。当然要讲他的其他杰出贡献,那就要把话题扯远了,今天我们只讲讲Dijkstra算法的思想,然后在后面给出它的实现过程,必要的给出其正确性的证明(Dijkstra在程序正确性证明领域发明了最弱前置谓词证明方法,不过我们不会用他的方法证明他老人家的程序的正确性,不然就扯到了十万八千里-。-)。

  算法思想:

   (1)在任意时刻,我们都要得到从源点到所有顶点的估算距离,并维持一个顶点集合S,若顶点v在S中,则说明从源点到v的最短路径已知;

   (2)在每一次将不在S中的顶点v加到S中去时,总是选择从源点到v的估算距离最小的;

   (3)顶点v加入S中之后,对于所有与v相邻的顶点(不属于S),更新它们的估算距离。

  由(2),我们看到了贪心的影子,在每次选择时,我们总是想选择花费最小的,正常人都会这样去想,至于为什么这样选,这样选对不对,我们将在后面进行证明。

  伪代码如下:

1 Dijkstra(G, W, s)      //G表示图,W表示权值函数,s表示源顶点 2   d[s] ←0          //源点到源点最短路为0 3   for each v ∈ V - {s}  //3-8行均为初始化操作 4      do d[v]←∞ 5        parent[v]←NIL 6   S←∅         7   Q←V        //此处Q为优先队列,存储未进入S的各顶点以及从源点到这些顶点的估算距离,采用二叉堆(最小堆)实现,越小越优先 8   while Q≠∅ 9    do u←Extract-Min(Q)  //提取估算距离最小的顶点,在优先队列中位于顶部,出队列,放入集合S中 10     S←S∪{u} 11     for each v ∈ Adj(u)  //松弛操作,对与u相邻的每个顶点v,进行维持三角不等式成立的松弛操作。 12       do if d[v] > d[u] + w(u, v) 13         then d[v] = d[u] + w(u, v)  //这一步隐含了更新优先队列中的值,DECREASE。 14            parent[v]←u      //置v的前驱结点为u

六、简单例子说明

  初始情况:

第一次松弛,选取A顶点:

 

第二次松弛,C的估算距离最小,选取C顶点:

第三次松弛,E的估算距离最小,选取E:

第四次松弛,B的估算距离最小,选取B:

第五次松弛:(最后一个点,完成)

  经过所有的松弛操作之后,我们就得到了所有顶点的最短路径(表格中红字部分)。

  如果加上对parent[]进行的操作,我们还可以得到一棵最短路径树,这个读者可以自行推广。

七、代码实现

  相比正确性,可能大家更加关注的是代码的实现,这里我们给出Dijkstra的实现代码,图结构的存储采用邻接矩阵和邻接表两种形式,有关图的表示方法,可以参看下面的博客:http://www.cnblogs.com/dzkang2011/p/graph_1.html。优先队列采用C++ 优先队列STL,priority_queue,由于无法直接更改队列中的值,我们需要对实现进行稍微的修改,这对最后结果不会产生影响,详情见代码。

  1、邻接表C/C++:

1 #include 2 #include 3 #include 4 #include 5 using namespace std; 6 7 #define maxn 110 //最大顶点个数 8 int n; //顶点个数 9 10 struct arcnode //边结点 11 { 12 int vertex; //与表头结点相邻的顶点编号 13 int weight; //连接两顶点的边的权值 14 arcnode * next; //指向下一相邻接点 15 arcnode() {} 16 arcnode(int v,int w):vertex(v),weight(w),next(NULL) {} 17 }; 18 19 struct vernode //顶点结点,为每一条邻接表的表头结点 20 { 21 int vex; //当前定点编号 22 arcnode * firarc; //与该顶点相连的第一个顶点组成的边 23 }Ver[maxn]; 24 25 void Init() //建立图的邻接表需要先初始化,建立顶点结点 26 { 27 for(int i = 1; i vertex == b) 43 { 44 if(p->weight > w) 45 p->weight = w; 46 return ; 47 } 48 while(p->next != NULL) 49 { 50 if(p->next->vertex == b) 51 { 52 if(p->next->weight > w) 53 p->next->weight = w; 54 return ; 55 } 56 p = p->next; 57 } 58 p->next = q; 59 } 60 } 61 void Insert2(int a, int b, int w) //头插法,效率更高,但不能去重边 62 { 63 arcnode * q = new arcnode(b, w); 64 if(Ver[a].firarc == NULL) 65 Ver[a].firarc = q; 66 else 67 { 68 arcnode * p = Ver[a].firarc; 69 q->next = p; 70 Ver[a].firarc = q; 71 } 72 } 73 struct node //顶点节点,保存id和到源顶点的估算距离,优先队列需要的类型 74 { 75 int id; //源顶点id和估算距离 76 int w; 77 friend bool operator b.w; 80 } 81 }; 82 83 #define INF 0xfffff //权值上限 84 int parent[maxn]; //每个顶点的父亲节点,可以用于还原最短路径树 85 bool visited[maxn]; //用于判断顶点是否已经在最短路径树中,或者说是否已找到最短路径 86 node d[maxn]; //源点到每个顶点估算距离,最后结果为源点到所有顶点的最短路。 87 priority_queue q; //优先队列stl实现 88 void Dijkstra(int s) //Dijkstra算法,传入源顶点 89 { 90 for(int i = 1; i vertex; 112 if(!visited[v] && d[v].w > d[u].w+p->weight) 113 { 114 d[v].w = d[u].w+p->weight; 115 parent[v] = u; 116 q.push(d[v]); 117 } 118 p = p->next; 119 } 120 } 121 } 122 123 int main() 124 { 125 int m, a, b, c, st, ed; 126 printf("请输入顶点数和边数:\n"); 127 scanf("%d%d", &n, &m); 128 printf("请输入边以及权值(a, b, c)\n"); 129 Init(); //计算前必须初始化 130 while(m--) 131 { 132 scanf("%d%d%d", &a, &b, &c); 133 Insert2(a, b, c); //无向图注意存储两条边 134 Insert2(b, a, c); 135 } 136 printf("请输入起点和终点:\n"); 137 scanf("%d%d", &st, &ed); 138 Dijkstra(st); 139 if(d[ed].w != INF) 140 printf("最短路径权值为:%d\n", d[ed].w); 141 else 142 printf("不存在从顶点%d到顶点%d的最短路径。\n", st, ed); 143 return 0; 144 }

  2、邻接矩阵C/C++:

1 #include 2 #include 3 #include 4 using namespace std; 5 6 #define maxn 110 //最大顶点个数 7 #define INF 0xffffff //权值上限 8 int w[maxn][maxn]; //邻接矩阵,存储权值 9 int n; //顶点个数 10 11 struct node //顶点节点,保存id和到源顶点的估算距离,优先队列需要的类型 12 { 13 int id, weight; //源顶点id和估算距离 14 friend bool operator b.weight; 17 } 18 }; 19 priority_queue q; //优先队列,最小堆,实现Dijkstra的重要数据结构,用stl实现 20 int parent[maxn]; //每个顶点的父亲节点,可以用于还原最短路径树 21 bool visited[maxn]; //用于判断顶点是否已经在最短路径树中,或者说是否已找到最短路径 22 node d[maxn]; //源点到每个顶点估算距离,最后结果为源点到所有顶点的最短路。 23 void Dijkstra(int s) //Dijkstra算法,传入源顶点 24 { 25 for(int i = 1; i c) 67 w[a][b]= w[b][a] = c; 68 } 69 printf("请输入起点和终点:\n"); 70 scanf("%d%d", &st, &ed); 71 Dijkstra(st); 72 if(d[ed].weight != INF) 73 printf("最短路径权值为:%d\n", d[ed].weight); 74 else 75 printf("不存在从顶点%d到顶点%d的最短路径。\n", st, ed); 76 return 0; 77 }

 八、时间复杂度分析

 

  不管用什么方法,总共用时为O(V*T(EXTRACTION)+E*T(DECREASE))

 

    (1)如果用数组来实现,总时间复杂度为O(V2)

 

    (2)如果用二叉堆来实现,总时间复杂度为O(ElogV)

 

    (3)如果使用斐波那契堆,总时间复杂度为O(E+VlogV)

 

    上面的三种方法,越往下时间复杂度越好,但是实现难度越高,而且每次对最小优先队列的更新是非常麻烦的,那么,有没有一种方法,可以不更新优先队列也达到同样的  效果呢?

 

    答案是:有。

 

    其实只需要简单的操作就可以达到。首次只将根结点入队列。第一次循环,取出队列顶结点,将其退队列,之后找到队列顶的结点的所有相邻顶点,若有更新,则更新它们  的key值后,再将它们压入队列。重复操作直至队列空为止。因为对树的更新是局部的,所以只需将相邻顶点key值更新即可。push操作的复杂度为O(logV),而且省去了之前  将所有顶点入队列的时间,因而总复杂度为O(ElogV)。

 



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭