第一句子网 - 唯美句子、句子迷、好句子大全
第一句子网 > 【数据结构与算法基础】最短路径问题

【数据结构与算法基础】最短路径问题

时间:2021-06-17 18:48:32

相关推荐

【数据结构与算法基础】最短路径问题

前言

数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷。

也因如此,它作为博主大二上学期最重要的必修课出现了。由于大家对于上学期C++系列博文的支持,我打算将这门课的笔记也写作系列博文,既用于整理、消化,也用于同各位交流、展示数据结构的美。

此系列文章,将会分成两条主线,一条“数据结构基础”,一条“数据结构拓展”。“数据结构基础”主要以记录课上内容为主,“拓展”则是以课上内容为基础的更加高深的数据结构或相关应用知识。

欢迎关注博主,一起交流、学习、进步,往期的文章将会放在文末。

说起图论算法,最经典的莫过于最短路径了。他永远是图论入门难以绕开的经典问题,也常常是学习图论遇到的容易卡住的难点。

说起最短路径,那首先研究的对象就是两点间的路径,它要有起点和终点。图中两个连通结点之间至少存在一条路径,最短路径就是其中权值和最小的路径。

算法中对权值定义可以很广泛,他可以是点权,也可以是边权,也可能多关键字的权重…总之不论权值如何定义,重要的只是权值和能够被表示出来且能够被比较。但在本文中,我们讨论的只有边权,且权值为正数的正权图。

这篇文章,我们探讨的最短路径分为两种:单源最短路径任意两点间最短路径

以及实现他们的两种经典算法:dijkstra算法和floyd算法

单源最短路径

单源最短路径,是指从一点出发到图中其他所有点的最短路径。

通常,只要求出到各点的最短路径长度。

有时,也会要求打印出最短路径。

例如:对于如下图

从源点1出发到其他节点的最短路径及距离为:

2:1,3,232:1,3,2\quad32:1,3,23

3:1,313:1,3\quad13:1,31

4:1,3,454:1,3,4\quad54:1,3,45

5:1,3,565:1,3,5\quad65:1,3,56

6:1,3,5,676:1,3,5,6\quad76:1,3,5,67

求解单源最短路径,可行的算法有很多,下面来介绍一种非常经典的算法:dijkstra算法

迪杰斯特拉(dijkstra)算法

dijkstra算法的核心思想是将全部结点分成两个集合,一个是已知最短路的集合AAA,剩下的是未知最短路的集合BBB,同时记录每个节点到源点的最短距离disdisdis。

约定:源点到自身的最短距离为0,即diss=0dis_s = 0diss​=0。无路径的两点间的距离为无穷大。

算法步骤如下:

一开始,已知最短路的集合中仅有源点一个元素。且仅记录与源点直接相连的节点的距离disdisdis,其余结点默认为无穷大每次从BBB中找出一个距离最短的结点kkk,加入集合AAA。使用结点kkk更新源点到BBB中结点的最短距离,即是否经过kkk到该结点比先前到该结点的方案更优重复上述过程,直至所有结点都加入结合AAA最短路径长度就是结点的disdisdis值

对于上图,将每一次挑选节点的过程中disdisdis用表格画出来就是:

在编写算法之前,有必要先来说明一下图的存储。有关图的存储方式,可以参考这篇博文

下面的算法中,存储方式都采用数组模拟的邻接表的方式。最短路径使用数组dis进行存储;

代码定义如下:

struct Edge{//邻接表边结构体int vertex;//终点编号int len;//边权int next;//后继边在数组中下标};Edge nxt[M];int head[N];//链首数组int dis[N];//最短路长度int tot = 0;//边计数bool vis[N];//标记点是否已经计算出最短路径

朴素实现

朴素的算法就是每次使用遍历的方式查询BBB集合中距离最小的结点

void dijkstra(int start,int n){const int inf = 1 <<29;//初始化结点信息for(int i = 0;i < n;i++){dis[i] = inf;vis[i] = false;}//设置起点距离为0dis[start] = 0;int k;//选中的距离最小结点while(true){k = -1;for(int i = 0;i < n;i++){//遍历寻找距离最小结点if(vis[i] || dis[i] == inf){//已经在集合中,或者不可到达的元素不考虑continue;}if(k == -1 || dis[i] < dis[i]){k = i;}}if(k == -1){//不存在集合外的元素,即所有结点都寻找到了最短路break;}vis[k] = true;//将该节点加入集合for(int i = head[k];i;i = nxt[i].next){//遍历以该节点为起点的所有边,更新与改点相连结点最短路径if(vis[nxt[i].vertex]){//如果该节点已经在集合中,则跳过continue;}if(dis[nxt[i].vertex] > nxt[i].len + dis[k]){//如果从当前结点出发更优则更新答案dis[nxt[i].vertex] = nxt[i].len + dis[k];}}}}

朴素算法的确就是按照算法步骤逐步翻译即可得到。但是缺点也很明显,就是复杂度太高了。

总的来看,外层循环每次会加入一个节点,内层中每次需要遍历所有结点获取最近结点。另外,他还遍历了所有边。

所以如果点的数量为nnn,边的数量为mmm,则算法复杂度为:O(n2+m)O(n^2 +m)O(n2+m)

堆优化实现

使用堆可以优化算法的寻找最短路径最小点的过程。特别的,我们需要定义堆中的结点,它现在不仅是单个的数据,而是一系列数据的复合体,同时这个复合体应该可以进行比较从而构建堆(对于堆得定义和实现还不太理解的同学可以先回顾这一篇):

堆结点定义及堆实现如下:

struct Node{int vertex;//到某个结点int dis;//到该节点的最短距离bool operator < (const Node & node){return dis < node.dis;}}Node heap[M];//堆空间int size = 0;//堆规模void down(int k){int son = k << 1;while(son <= size){if(son + 1 <= size && heap[son + 1] < heap[son]){son++;}if(heap[k] < heap[son]){break;}Node temp = heap[k];heap[k] = heap[son];heap[son] = temp;k = son;son <<= 1;}}void up(int k){int fa = k >> 1;while(fa != 0){if(heap[fa] < heap[k]){break;}Node temp = heap[fa];heap[fa] = heap[k];heap[k] = temp;k = fa;fa >>= 1;}}void add(Node node){heap[++size] = node;up(size);}void pop(){heap[1] = heap[size--];down(1);}Node peak(){return heap[1];}

有了堆的定义,下面就是如何使用堆来优化算法了:

算法开始时将源点信息初始化并入堆 每次从堆顶中拿去最短路径最小元素并将其弹出如果该节点已经在集合中,则重复第一步将节点加入集合遍历所有可到达的结点,如果可以使用当前结点更新最短路径,则将更新后的结果加入堆中

从上面的过程中可以看到使用堆优化的算法存在一个小问题,就是一个节点在堆中可能出现多次,每次被结点更新路径都会导致其加入堆中。但这并不会影响算法的正确性,因为最终一定会先拿到结点的最短路径,后续再拿到该节点则令其直接弹出。

void dijkstra(int start,int n){const int inf = 1 <<29;//初始化结点信息for(int i = 0;i < n;i++){dis[i] = inf;vis[i] = false;}//设置起点距离为0dis[start] = 0;add({0,0});//加入源点Node node;//选中的距离最小结点while(size){//循环,堆不为空node = peak();//获取当前堆顶元素pop();if(vis[node.vertex]){//如果该对顶元素已经在集合中continue;}vis[k] = true;//将该节点加入集合for(int i = head[k];i;i = nxt[i].next){//遍历以该节点为起点的所有边,更新与改点相连结点最短路径if(vis[nxt[i].vertex]){//如果该节点已经在集合中,则跳过continue;}if(dis[nxt[i].vertex] > nxt[i].len + dis[k]){//如果从当前结点出发更优则更新答案dis[nxt[i].vertex] = nxt[i].len + dis[k];add({nxt[i].vertex,dis[nxt[i].vertex]});//将被更新结点新状态加入堆中}}}}

使用堆优化算法虽然在堆得构建上耗费了不少功夫,但是在算法效率的优化上却改进了不少,里层的寻找最短路径点的步骤复杂度变成了lognlognlogn所以最终的复杂度为O(nlogn)O(nlogn)O(nlogn)。

使用单调队列优化实现

使用堆很复杂,因为手写堆得实现。那么有没有办法即使用堆得优化,又不用手写堆得代码呢?

答案是有的,使用标准库中封装好的容器priorit_queue即优先队列可以代替上面的堆。

使用方法就是给队列结点定义一个比较函数(重载小于号),引入头文件queuequeuequeue,声明一个泛型为Node的优先队列。

#include<queue>struct Node{int vertex;//到某个结点int dis;//到该节点的最短距离bool operator < (const Node & node){return dis < node.dis;}}priority_queue<Node> pq;//声明一个泛型为Node的优先队列

然后直接调用其封装好的方法即可:

void dijkstra(int start,int n){const int inf = 1 <<29;//初始化结点信息for(int i = 0;i < n;i++){dis[i] = inf;vis[i] = false;}//设置起点距离为0dis[start] = 0;pq.push({0,0});Node node;//选中的距离最小结点while(!pq.empty()){//循环,堆不为空node = pq.top();//获取当前堆顶元素pq.pop();if(vis[node.vertex]){//如果该对顶元素已经在集合中continue;}vis[k] = true;//将该节点加入集合for(int i = head[k];i;i = nxt[i].next){//遍历以该节点为起点的所有边,更新与改点相连结点最短路径if(vis[nxt[i].vertex]){//如果该节点已经在集合中,则跳过continue;}if(dis[nxt[i].vertex] > nxt[i].len + dis[k]){//如果从当前结点出发更优则更新答案dis[nxt[i].vertex] = nxt[i].len + dis[k];pq.push({nxt[i].vertex,dis[nxt[i].vertex]});//将被更新结点新状态加入堆中}}}}

这样一来算法的实现就便捷多了。

但是stl虽好,也不要贪杯哦,尤其是对刚学习数据结构的同学,手写几遍堆绝对是百利而无一害的。

任意两点间最短路径

除了单源的最短路径,有时我们还会关心任意两点间的最短路径。这时源点就从单个确定的点变成了所有的点,要求出所有点到所有点的最短路径。

这使得我们不禁想到刚刚讨论的dijkstra算法可以求出单源最短路径,如果对每个点都使用一遍这个算法,是不是就能够得出任意两点间的最短路径了呢?

答案是肯定的,对每个点都跑一遍dijkstra确实可以得到任意两点间的最短路径。其复杂度为:O(n2logn)O(n^2logn)O(n2logn)

但是问题的关键在于,它挺麻烦的,你看dijkstra算法写起来这么老长。

弗洛伊德(floyd)算法

floyd算法也是求解任意两点间最短路径的算法 ,而他的思想就要简洁优雅的多。

他利用动态规划的思想,每次枚举中间点并使用其更新其他点对之间的距离。

可以证明的是,当遍历所有中间点后,任意两点间的距离就是最短距离。

该算法的复杂度也不难分析:枚举中间节点复杂度为O(n)O(n)O(n),枚举点对复杂度O(n2)O(n^2)O(n2),总体算法复杂度为O(n3)O(n^3)O(n3)

接着我们来看floyd算法的代码实现,在floyd算法中,常使用邻接矩阵来记录两点间距离:

//邻接矩阵,对角线为0,无连边的点对之间距离为infint dis[N][N];for(int k = 0;k < n;k++){//枚举中间节点for(int i = 0;i < n;i++){//枚举点对for(int j = 0;j < n;j++){dis[i][j] = min(dis[i][j],dis[i][k] + dis[k][j]);//更新点对之间的最短距离}}}

跑完这个循环,任意两点间的最短路径就寻找完毕了,比跑n边dijkstra要简洁的多,对吧。不过需要注意的就是该算法只推荐使用在点较少的图中,一般这个规模限制在1000之内,这样邻接矩阵开的下,运行时间也不会超。如果规模进一步变大,那就老老实实用dijkstra吧。

往期博客

【数据结构基础】数据结构基础概念【数据结构基础】线性数据结构——线性表概念 及 数组的封装【数据结构基础】线性数据结构——三种链表的总结及封装【数据结构基础】线性数据结构——栈和队列的总结及封装(C和java)【算法与数据结构基础】模式匹配问题与KMP算法【数据结构与算法基础】二叉树与其遍历序列的互化 附代码实现(C和java)【数据结构与算法拓展】 单调队列原理及代码实现【数据结构基础】图的存储结构【数据结构与算法基础】并查集原理、封装实现及例题解析(C和java)【数据结构与算法拓展】二叉堆原理、实现与例题(C和java)【数据结构与算法基础】哈夫曼树与哈夫曼编码(C++)

参考资料:

《数据结构》(刘大有,杨博等编著)《算法导论》(托马斯·科尔曼等编著)《图解数据结构——使用Java》(胡昭民著)OI WiKi

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。