图详解第四篇:单源最短路径--Dijkstra算法
文章目录
- 1. 最短路径问题
- 2. 单源最短路径--Dijkstra算法
- 算法思想
- 图解
- 如何存储路径及其权值
- 代码实现
- 调式观察
- 打印最短路径
- Dijkstra算法的缺陷
- 3. 源码
1. 最短路径问题
最短路径问题:
从带权有向图(求最短路径通常是有向图)G中的某一顶点出发,找出一条通往另一顶点的最短路径,最短也就是沿路径各边的权值总和达到最小。
那下面我们就要来学习几个求最短路径的算法
2. 单源最短路径–Dijkstra算法
这篇文章我们先来学习第一个求单源最短路径的算法——迪杰斯特拉算法(Dijkstra),是由荷兰计算机科学家狄克斯特拉于1959年提出的,然后后面我们还会学到求多源最短路径的算法。
所以这里先给大家介绍一下什么是单源最短路径,什么是多源最短路径:
单源最短路径指的是从一个源节点出发,计算到其他所有节点的最短路径。也就是说,在单源最短路径问题中,只需要确定一个起点,然后计算该起点到图中所有其他节点的最短距离。
多源最短路径则是在图中计算任意两个节点之间的最短路径。换言之,需要求解所有可能的起点和终点之间的最短路径。
那下面我们就来学习一下第一个求单源最短路径的算法——Dijkstra算法
算法思想
首先我们可以先从概念上了解一下Dijkstra算法的思想:
单源最短路径问题:给定一个图G = ( V , E ) ,求源结点s ∈ V 到图中每个结点v ∈ V的最短路径。Dijkstra算法就适用于解决带权重的有向图上的单源最短路径问题,同时算法要求图中所有边的权重非负。一般在求解最短路径的时候都是已知一个起点和一个终点,所以使用Dijkstra算法求解过后也就得到了所需起点到终点的最短路径。
针对一个带权有向图G,将所有结点分为两组S和Q,S是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点s放入,毕竟源节点到自己的代价是0),Q 为其余未确定最短路径的结点集合,每次从Q 中找出一个从起点到该结点代价最小的结点u ,将u 从Q 中移出,并放入S 中,对u 的每一个相邻结点v (且v不在S中)进行松弛操作。松弛即对每一个相邻结点v ,判断源节点s到结点u 的代价与u 到v 的代价之和是否比原来s 到v 的代价更小,若代价比原来小则要将s 到v 的代价更新为s 到u 与u 到v 的代价之和,否则维持原样。如此一直循环直至集合Q 为空,即所有节点都已经查找过一遍并确定了最短路径,至于一些起点到达不了的结点在算法循环后其代价仍为初始设定的值,不发生变化。
Dijkstra算法每次都是选择V-S中最小的路径节点来进行更新,并加入S中,所以该算法使用的是贪心策略。
Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径,这个我们后面也会给大家演示。
图解
那只看上面的概念的话,大家可能还不是特别理解,那下面我们来画图带大家分析一下
首先,我们可以先来看一下算法导论上给出的图解:
大家可以自己先看一下
然后,我来带大家走一遍这个过程:
其实就按照上面的思想一步步走就行了。
按照上面说的,将所有结点分为两组S和Q,S是已经确定最短路径的结点集合,Q 为其余未确定最短路径的结点集合。
那起始的时候,可以认为S是空的,所有结点都在Q里面。
然后这里选择的起点是s
每次从Q 中找出一个从起点到该结点代价最小的结点u,那第一次这个结点u就是s,可以认为s到s的距离是0(图中每个结点里面的值就表示当前从起点到自己的最短路径,还没更新的路径用∞
标识),那把s结点放到S集合里面,Q中删去s;
然后对s 的每一个相邻结点v 进行松弛操作(其实去更新起点到它相邻点的距离),s到它相邻的两个结点距离s-t为10,s-y为5,都比原来从起点到它们的距离小,所以更新
然后再从Q里面找一个到起点路径最短的点,那这次找到的是y(此时s-y为5是最小的),把y从Q中移除,放入S里面;
然后对y进行松弛操作
y相邻的几个顶点到y的距离+y到起点s的距离都比之前起点到它们的距离短,所以都更新
接着继续从Q中选一个到起点距离最短的是z,z从Q中移出,放入S;
接着对x进行松弛操作,更新相应的距离
接着继续从Q中选一个到起点距离最短的是t,t从Q中移出,放入S;
接着对t进行松弛操作,更新相应的距离
再接着继续从Q中选一个到起点距离最短的是x,x从Q中移出,放入S;
接着再对x进行松弛操作
至此,集合Q 为空(起始Q是满的,所以n个结点的话,其实就选了n次去更新),即所有节点都已经查找过一遍并确定了最短路径
算法执行结束!
如何存储路径及其权值
相信算法现在大家已经理解了,但是还有一些问题需要我们解决:
既然我们是要求最短路径,那肯定得把相关的信息存储起来啊,上面图中直接把每个顶点对应最短路径的权值直接写到了结点里面,而且每条路径是怎么走的,经过了哪些顶点,我们也很容易看出来。
可是后面我们要写代码,那在写代码的时候我们如何把这些信息也存储起来呢?
🆗,是这样处理的:
因为每个顶点不是都映射一个下标嘛,所以我们就可以搞一个数组,每个下标映射的顶点是谁,这个位置就存储起点到这个顶点的最短路径。
那最开始就是这样的:
然后后面我们每次更新最短路径的时候修改里面的权值就行了
那上面存的是最短路径的权值,那路径又要如何存储呢?
一条路径可能会经过多个顶点啊。
🆗,那这里呢还是用一个一维数组就可以搞定:
怎么做呢?
很简单,每个顶点映射的位置存储路径上在它前面的那个顶点映射的下标,如果把路径看成一棵树的话,就是存储它的父结点的下标
比如最开始就可以这样存
首先s自己就是起点,可以认为最短路径就是s->s,所以它存自己的下标,然后剩下的顶点都还没有更新最短路径,起始存一个-1
接着每走一步就去更新数组就行了(存路径上它前面的那个结点(可以认为是它的父结点)的下标,类似并查集那里用的双亲表示法存储),那到最后的时候,就是这样的
那这样存储路径的话我们想要获取一个顶点的最短路径的时候,就从这个顶点开始顺序它的父亲(路径中的上一个结点)往上找就行了,找到起点停止,就是一条完整的路径(类似并查集里面的findRoot顺着父结点向上找根)。
代码实现
那下面我们就来实现一下代码:
首先需要给一个起点,然后两个vector存储最短路径及对应的路径权值
然后,按照我们上面分析的思路走就行了
注释写的比较清楚,相信大家应该很容易可以看懂,说一点就是我们现在用的是邻接矩阵结构,所有查找u相邻的结点是去邻接矩阵_matrix
里面找,如果下标[u][v]
的位置对应的权值不是MAX_W
,那它们就相连的,v就是u的一个相邻顶点,然后再判断如果源节点s到结点u 的代价与u 到v 的代价之和(其实就是距离嘛)是否比原来s 到v 的代价更小,若代价比原来小则要将s 到v 的代价更新为s 到u 与u 到v 的代价之和(更新距离)
调式观察
那这就实现好了,我们可以先通过调式观察一下:
我这里已经写好了一个测试用例(最后会给大家分享源码),这个图就是我们上面讲解思想的时候对应的那个图
我们来调式观察一下
对比一下我们之前分析的
没有问题,一模一样
打印最短路径
那下面呢我们可以写一个大于路径的函数,把最终得到的起点到各顶点的最短路径以及权值都打印出来看一下,和上面图上的是否一样:
那我们拿到这两个数组就可以去打印
但是这里打印的时候有一个问题:
按我们上面说的,找路径的时候通过pPath数组顺着结点的父亲或者说它的路径上的前一个结点,往上找就行了,找到起点停止。
但是!
这样找出来的路径是不是反的啊,因为我们最后找到的是起点,而正常情况应该是从起点开始嘛。
所以我们要处理一下,也很好搞:
我们可以搞一个vector把路径上的结点保存下来,然后逆置一下,再去打印就行了
来实现一下:
然后我们来打印一下看看:
大家可以对照一下,没有问题
Dijkstra算法的缺陷
但是呢,Dijkstra算法是有一些缺陷的,对于带有负权值的边的图,Dijkstra算法是搞不定的!
这里呢也准备了一个测试用例,我们可以来看一下:
首先它对应的一个图应该是这样的:
那我们现在对这个图执行Dijkstra算法并打印出来看一下:
是这样一个结果
但是我们会发现如果按照这个图有负权值的话,其实有一条最短路径其实没有更新出来,s->t->y应该是3(10+(-7)=3)
也就是说3应该是起点s到y的最短距离,s->t->y是最短路径。
图中带有负权路径时,贪心策略就失效了。
那为什么会这样呢?
因为按照Dijkstra算法的话
这里起点是s,所以第一次选到s,放到S集合里面,然后对s的相邻顶点进行松弛操作,更新距离s->t为10,s-y为5,所以第二次选到y,那y就被放到S集合里面了,而S是已经确定最短路径的结点集合,所以它这里的贪心策略就认为此时的5就是s->y的最短路径距离了(当然如果没有负权值的话这样是肯定正确的),y的最短路径已经确定了。
所以后面再去对t的相邻顶点进行松弛的时候就不会判断st+ty的距离是否小于sy,也不会再更新y的最短路径了,所以上面s->t->y就没有更新出来。因为每次都是从Q里面选的,而y前面已经放到S集合里面了。
但是有了负权值的话,sy的最短路径就不一定是5了(如果全是正的话肯定没问题),后面绕到其它的路径如果遇到负权值就可能会比5还小。
所以对于有负权值的图,Dijkstra算法就不再适用,这种贪心策略就失效了。
那对于有负权值的图我们如何求最短路径呢?
bellman—ford算法可以解决负权图的单源最短路径问题
这个我们下一篇文章就会讲到…
3. 源码
void Dijkstra(const V& src, vector<int>& pPath, vector<W>& dist)
{//初始化一下记录路径和权值(距离)的数组size_t srci = GetVertexIndex(src);size_t n = _vertexs.size();pPath.resize(n, -1);dist.resize(n, MAX_W);//集合S为已确定最短路径的顶点集合,这里我们用一个数组表示S集合就可以//,初始化全false(S中无结点),表示所有顶点都在Q中,//不在S就在Q(其余未确定最短路径的结点集合)vector<bool> s(n, false);pPath[srci] = srci;dist[srci] = 0;//n个结点,需要选择n次去更新路径for (size_t i = 0; i < n; i++){int u = 0;W minDist = MAX_W;//从Q 中找出一个从起点到该结点代价最小的结点ufor (size_t j = 0; j < n; j++){if (s[i] == false && dist[i] < minDist){u = i;minDist = dist[i];}}//将u 从Q 中移出,并放入S 中s[u] = true;//对u 的每一个相邻结点v 进行松弛操作,如果src->u + u->v < src->v ,就更新距离for (size_t v = 0; v < n; v++){if (s[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v]){dist[v] = dist[u] + _matrix[u][v];//同时更新记录路径的数组pPathpPath[v] = u;}}}}void ptintMinPath(const V& src, const vector<int>& pPath, const vector<W>& dist)
{size_t srci = GetVertexIndex(src);size_t n = _vertexs.size();for (size_t i = 0; i < n; i++){if (i != srci)//起点-》起点就没必要打印了{//定义一个vector保存路径上的结点vector<int> path;//从当前结点开始顺着父结点往上走size_t parenti = i;while (parenti != srci){path.push_back(parenti);parenti = pPath[parenti];}path.push_back(srci);//逆置path数组reverse(path.begin(), path.end());//打印路径for (auto e : path){cout << _vertexs[e] << "->";}//打印权值cout << dist[i] << endl;}}
}
void TestGraphDijkstra()
{/*const char* str = "syztx";Graph<char, int, INT_MAX, true> g(str, strlen(str));g.AddEdge('s', 't', 10);g.AddEdge('s', 'y', 5);g.AddEdge('y', 't', 3);g.AddEdge('y', 'x', 9);g.AddEdge('y', 'z', 2);g.AddEdge('z', 's', 7);g.AddEdge('z', 'x', 6);g.AddEdge('t', 'y', 2);g.AddEdge('t', 'x', 1);g.AddEdge('x', 'z', 4);vector<int> dist;vector<int> pPath;g.Dijkstra('s', dist, pPath);g.ptintMinPath('s', dist, pPath);*/// 图中带有负权路径时,贪心策略则失效了。// 测试结果可以看到s->t->y之间的最短路径没更新出来const char* str = "sytx";Graph<char, int, INT_MAX, true> g(str, strlen(str));g.AddEdge('s', 't', 10);g.AddEdge('s', 'y', 5);g.AddEdge('t', 'y', -7);g.AddEdge('y', 'x', 3);vector<int> dist;vector<int> pPath;g.Dijkstra('s', dist, pPath);g.ptintMinPath('s', dist, pPath);
}
相关文章:

图详解第四篇:单源最短路径--Dijkstra算法
文章目录 1. 最短路径问题2. 单源最短路径--Dijkstra算法算法思想图解如何存储路径及其权值代码实现调式观察打印最短路径Dijkstra算法的缺陷 3. 源码 1. 最短路径问题 最短路径问题: 从带权有向图(求最短路径通常是有向图)G中的某一顶点出发…...

CRMEB多商户商城系统阿里云集群部署教程
注意: 1.所有服务创建时地域一定要选择一致,这里我用的是杭州K区 2.文件/图片上传一定要用类似oss的云文件服务, 本文不做演示 一、 创建容器镜像服务,容器镜像服务(aliyun.com) ,个人版本就可以 先创建一个命名空间 然后创建一个镜像仓库 查看并记录镜像公网地址…...
Java第三方登录封装工具类
Java中可以使用第三方登录来简化用户登录流程,常见的第三方登录如QQ、微信、微博等。下面是一个Java封装第三方登录的工具类: import java.io.IOException; import java.util.HashMap; import java.util.Map;import org.apache.http.client.ClientProto…...

BUUCTF学习(四): 文件包含tips
1、介绍 2、hackBar插件安装 教程: Hackbar插件安装-CSDN博客 3、解题 php://filter/readconvert.base64-encode/resource要读取的文件 ?filephp://filter/readconvert.base64-encode/resourceflag.php Base64 在线编码解码 | Base64 加密解密 - Base64.us 结束...

德国人工智能公司【Kodex AI】完成160万欧元融资
来源:猛兽财经 作者:猛兽财经 猛兽财经获悉,总部位于德国柏林的人工智能公司【Kodex AI】今日宣布已完成160万欧元融资。 本轮融资由Signals VC领投,Techstars、德意志银行等天使投资者参与,其中包括:most AI首席执行官…...

LeetCode 2 两数相加
题目描述 链接:https://leetcode.cn/problems/add-two-numbers/?envTypefeatured-list&envId2ckc81c?envTypefeatured-list&envId2ckc81c 难度:中等 给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式…...
springboot项目启动失败,不打印报错详细信息(启动打印日记问题)
1:出现这种我问题一般都是日记的问题,查看控制台启动打印的第一句,为什么启动失败,需要用那个日记 2:如果使用的是log4j或者logback与slf4j都是默认在依赖web自带的如下 <dependency><groupId>org.springf…...
MyBatis (where、set、foreach)标签
where标签 在上一节SQL 语句中加入了一个条件“11”,如果没有加入这个条件,那么可能就会变成下面这样一条错误的语句。 SELECT id,name,url,age,country FROM website AND name LIKE CONCAT(%,#{name},%)显然以上语句会出现 SQL 语法异常,但…...
flutter开发之安装dart
1、在MacOS系统中打开终端,进入到官网Get the Dart SDK | Dart brew tap dart-lang/dartbrew install dart 注意:若显示没有brew,请先执行第二步骤,如下: 2、打开homebrew的官网Homebrew — The Missing Package Man…...
向量召回:深入评估离线体系,探索优质召回方法
向量召回:深入评估离线体系,探索优质召回方法 1.简介 近年来,基于向量进行召回的做法在搜索和推荐领域都得到了比较广泛的应用,并且在学术界发表的论文中,基于向量的 dense retrieve 的方法也在不少数据集上都战胜了 sparse retrieve,吸引了越来越多的关注。在内网的不…...
播放器缓存队列bug解决方案
背景 我在开发一个播放器的缓存队列时,遇到一个bug,导致包和帧无法被下一个模块读取 找了半天,原来是队列中的包和帧数据要进行内容的刷新暂存 包数据和帧数据不能直接放入队列 //入队,包进队列 int AVPacketQueue::Push(AVPacket *val,i…...
React拖拽实践
当涉及到前端开发中的用户体验时,拖拽功能是一个常见而重要的需求。在React中,实现拖拽功能可以通过多种方式完成,但通常需要深刻理解React的状态管理、事件处理和DOM操作。本文将探讨React中拖拽的实践,包括基本原理、拖拽库的使…...

Stable Diffusion绘图,lora选择
best quality, ultra high res, (photorealistic:1.4), 1girl, off-shoulder white shirt, black tight skirt, black choker, (faded ash gray hair:1), looking at viewer, closeup <lora:koreandolllikeness_v20:0.66> 最佳品质,超高分辨率,&am…...

kube-controller-manager和kube-scheduler不能正常启动
kube-controller-manager-k8s-worker01和kube-scheduler-k8s-worker01没有启动起来 原因: 解决:进入/etc/kubernetes/manifests 编辑 将镜像地址修改为 然后重启kubelet:systemctl restart kubelet.service...

Mac OS m1 下安装Gradle5.1
1. 下载、解压 1.1 下载地址 https://gradle.org 往下翻 选择 5.1 或者选择 任何 你想要的版本 ,点击 binary-only 即可下载 . 1.2 解压到指定目录 2. 配置环境变量 2.1 编辑环境文件 vi ~/.bash_profile #GRADLE相关配置 GRADLE_HOME/Users/zxj/Documents/devSoft/grad…...
JUC并发编程面试题(自用)
线程池 1 线程池的作用:提高线程的利用率,线程复用,频繁的创建和销毁线程非常浪费资源 线程池的七大参数: corePoolSize(核心线程数):线程池中始终保持的活动线程数,即使它们处于空…...
Redis分布式会话
当探讨Redis分布式会话管理时,以下是更加详细的知识点: 1. 会话管理的挑战: 在分布式应用程序中,每个用户请求可能由不同的服务器处理。这导致了会话数据的分散性,需要一种方法来维护一致性的用户会话状态。 2. Redi…...

程序员大厂之鹅厂探秘
...

【Java 进阶篇】深入理解 JavaScript DOM Node 对象
在前端开发中,与HTML文档进行交互是一项基本任务。文档对象模型(Document Object Model,简称DOM)为开发者提供了一种以编程方式访问和操作HTML文档的方式。DOM的核心是节点(Node)对象,它代表了文…...

测试用例基础
测试用例的基本要素 测试环境, 操作步骤, 测试数据, 预期结果 测试用例的设计方法 基于需求的设计方法 需求文档 -> 梳理需求(掌握需求) -> 针对文档设计测试用例 只是针对需求进行大概的测试 具体的设计方法 等价类 等价类: 依据需求将输入(特殊情况…...

【人工智能】神经网络的优化器optimizer(二):Adagrad自适应学习率优化器
一.自适应梯度算法Adagrad概述 Adagrad(Adaptive Gradient Algorithm)是一种自适应学习率的优化算法,由Duchi等人在2011年提出。其核心思想是针对不同参数自动调整学习率,适合处理稀疏数据和不同参数梯度差异较大的场景。Adagrad通…...

盘古信息PCB行业解决方案:以全域场景重构,激活智造新未来
一、破局:PCB行业的时代之问 在数字经济蓬勃发展的浪潮中,PCB(印制电路板)作为 “电子产品之母”,其重要性愈发凸显。随着 5G、人工智能等新兴技术的加速渗透,PCB行业面临着前所未有的挑战与机遇。产品迭代…...

srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
linux 下常用变更-8
1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...
Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换
目录 关键点 技术实现1 技术实现2 摘要: 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式(自动驾驶、人工驾驶、远程驾驶、主动安全),并通过实时消息推送更新车…...

ui框架-文件列表展示
ui框架-文件列表展示 介绍 UI框架的文件列表展示组件,可以展示文件夹,支持列表展示和图标展示模式。组件提供了丰富的功能和可配置选项,适用于文件管理、文件上传等场景。 功能特性 支持列表模式和网格模式的切换展示支持文件和文件夹的层…...
用神经网络读懂你的“心情”:揭秘情绪识别系统背后的AI魔法
用神经网络读懂你的“心情”:揭秘情绪识别系统背后的AI魔法 大家好,我是Echo_Wish。最近刷短视频、看直播,有没有发现,越来越多的应用都开始“懂你”了——它们能感知你的情绪,推荐更合适的内容,甚至帮客服识别用户情绪,提升服务体验。这背后,神经网络在悄悄发力,撑起…...

C++中vector类型的介绍和使用
文章目录 一、vector 类型的简介1.1 基本介绍1.2 常见用法示例1.3 常见成员函数简表 二、vector 数据的插入2.1 push_back() —— 在尾部插入一个元素2.2 emplace_back() —— 在尾部“就地”构造对象2.3 insert() —— 在任意位置插入一个或多个元素2.4 emplace() —— 在任意…...

Linux 内存管理调试分析:ftrace、perf、crash 的系统化使用
Linux 内存管理调试分析:ftrace、perf、crash 的系统化使用 Linux 内核内存管理是构成整个内核性能和系统稳定性的基础,但这一子系统结构复杂,常常有设置失败、性能展示不良、OOM 杀进程等问题。要分析这些问题,需要一套工具化、…...