详解Dijkstra算法(含数学证明和优化)
Dijkstra算法简介:
Dijkstra算法是由荷兰计算机科学家Edsger Wybe Dijkstra于1959年提出的一种解决有向加权图中单源最短路问题的算法,其中要求加权图中不可有负权边。
Dijkstra算法步骤演示:
- 以如下的一张有向正权图G为例,规定:
- 起点为A
- 向量xy→表示从顶点x到顶点y的有向边\vec{xy}表示从顶点x到顶点y的有向边
- 向量的模∣∣xy→∣∣表示有向边xy→的权值向量的模\left|\vec{xy}\right|表示有向边\vec{xy}的权值
- 初始状态:
设起点到各点当前最短距离为Lk(k=A,B,C,D,E),则有:L_k (k=A,B,C,D,E),则有:LA=0L_A=0
并设此时LB至LE=+∞L_B至L_E=+\infty
则可列表:
LAL_A | LBL_B | LCL_C | LDL_D | LEL_E | |
---|---|---|---|---|---|
初始状态 | 0 | +∞+\infty | +∞+\infty | +∞+\infty | +∞+\infty |
- 第一次迭代:
从起点A点出发,更新起点到A的邻居(B、C、D)的当前最短距离(LkL_k)。此时:
对B:LB=∣∣∣AB→∣∣∣=10L_B=\left|\vec{AB}\right|=10 对C:
LC=∣∣∣AC→∣∣∣=3L_C=\left|\vec{AC}\right|=3 对D:
LD=∣∣∣AD→∣∣∣=20L_D=\left|\vec{AD}\right|=20
则可列表:
LAL_A | LBL_B | LCL_C | LDL_D | LEL_E | |
---|---|---|---|---|---|
第一次迭代后 | 0 | 10 | 3 | 20 | +∞+\infty |
- 第二次迭代:
找出第一次迭代后除已处理过的起点A外,LkL_k最小的点:C
从C出发,更新C邻居(B、E)的LkL_k值,此时:
对B:
经过C到达B的路径长度:L=LC+∣∣∣CB→∣∣∣=5L=L_C+\left|\vec{CB}\right|=5
∵LB=10>L\because L_B=10>L
∴更新LB=L=LC+∣∣∣CB→∣∣∣=5\therefore 更新L_B=L=L_C+\left|\vec{CB}\right|=5
对E:
经过C到达E的路径长度:L=LC+∣∣∣CE→∣∣∣=18L=L_C+\left|\vec{CE}\right|=18
∵LE=+∞>L\because L_E=+\infty>L
∴更新LE=L=LC+∣∣∣CE→∣∣∣=18\therefore 更新L_E=L=L_C+\left|\vec{CE}\right|=18
则可列表:
LAL_A | LBL_B | LCL_C | LDL_D | LEL_E | |
---|---|---|---|---|---|
第二次迭代后 | 0 | 5 | 3 | 20 | 18 |
- 第三次迭代::
找出第二次迭代后,除已处理过的A、C两点外,LkL_k最小的点:B
从B出发,更新B邻居(D)的LkL_k值,此时:
对D:
经过B到达D的路径长度:L=LB+∣∣∣BD→∣∣∣=10L=L_B+\left|\vec{BD}\right|=10
∵LD=20>L\because L_D=20>L
∴更新LD=L=LB+∣∣∣BD→∣∣∣=10\therefore 更新L_D=L=L_B+\left|\vec{BD}\right|=10
则可列表:
LAL_A | LBL_B | LCL_C | LDL_D | LEL_E | |
---|---|---|---|---|---|
第三次迭代后 | 0 | 5 | 3 | 10 | 18 |
- 第四次迭代:
找出第三次迭代后,除已处理过的A、B、C三点外,LkL_k最小的点:D
从D出发,更新D邻居(E)的LkL_k值,此时:
对E:
经过D到E的路径长度:L=LD+∣∣∣DE→∣∣∣=21L=L_D+\left|\vec{DE}\right|=21
∵LE=18<L\because L_E=18
∴不必更新,LE=18\therefore 不必更新,L_E=18
则可列表:
LAL_A | LBL_B | LCL_C | LDL_D | LEL_E | |
---|---|---|---|---|---|
第四次迭代后 | 0 | 5 | 3 | 10 | 18 |
- 第五次迭代:
找出第四次迭代后,除已处理过的A、B、C、D四点外,LkL_k最小的点:E
此时,E没有邻居,因此对E的处理直接结束
则可列表:
LAL_A | LBL_B | LCL_C | LDL_D | LEL_E | |
---|---|---|---|---|---|
第五次迭代后 | 0 | 5 | 3 | 10 | 18 |
- 五次迭代后,有向图G中所有的点都被处理过,算法终止,则整个迭代过程列表如下:
LAL_A | LBL_B | LCL_C | LDL_D | LEL_E | |
---|---|---|---|---|---|
初始状态 | 0 | +∞+\infty | +∞+\infty | +∞+\infty | +∞+\infty |
第一次迭代后 | 0 | 10 | 3 | 20 | +∞+\infty |
第二次迭代后 | 0 | 5 | 3 | 20 | 18 |
第三次迭代后 | 0 | 5 | 3 | 10 | 18 |
第四次迭代后 | 0 | 5 | 3 | 10 | 18 |
第五次迭代后 | 0 | 5 | 3 | 10 | 18 |
不难观察到,Dijkstra算法的特点主要是从起点开始,“由近及远,层层扩展”,越靠前处理的点离起点越近,最后一个处理的点一定离起点最远。
所以,依据算法每找到一个顶点后,该顶点对应的LkL_k值就是起点到该点的最短路径长度,且LkL_k在这之后不会被更改。
而最后一次迭代得到的所有LkL_k的值,就是由起点(亦称源点)A到各点的最短路径长度。
故Dijkstra算法解决的是有向图中的单源最短路问题。
算法概括:
步骤概括:
- 第一个核心步骤:找到当前未处理过的顶点中LkL_k最小的点VV,(由于起点到起点的消耗为0,所以算法开始时V必定代表起点);
- 第二个核心步骤:若V有邻居,则计算经过V的情况下起点到达各邻居的消耗LL,并选择是否更新VV邻居的LkL_k值。若没有邻居则对该点的处理结束
- 重复以上两个核心步骤,直到满足算法终止的条件:有向图中所有的点都被处理过。
流程图:
数学描述及证明
- Dijkstra算法的数学描述:
设全集UU:有向图中所有的点的集合。
设点集SS :已经找到最短路径的点的集合,初始状态下设仅有 起点∈S起点\in S。
设点集QQ:还未找到最短路径的点的集合,显然Q=U−SQ=U-S。
设LkL_k为当前情况下,起点经过SS中若干点到点kk的最短距离(k∈Uk\in U),初始L起点=0L_{起点}=0,其他均为+∞+\infty。
算法开始:- 从起点开始,沿某条弧(设权值为arcsarcs)找到起点的一个邻居nn
- 令Ln=min{Ln,L起点+arcs}L_n=min\{L_n,L_{起点}+arcs\}
- 按此方式更新起点所有邻居
- 在集合QQ中找到LkL_k最小的点vv,则LvL_v即起点到vv的最短路径长度
- 将点vv从QQ中取出加入SS,对点vv重复上述所有操作
- 如此重复,直到S=US=U,即Q=∅Q=\emptyset时,算法结束,LkL_k即为从起点到各点的最短路径长度
提醒:这里读者一定要反复仔细体会LkL_k的含义,它不断更新的过程正是Dijkstra算法“由近及远,层层扩展”特点的体现。同时思考一下之前提过的“找到一个点后,该点LkL_k值肯定不会被更改”的原因(理解LkL_k的含义后,原因其实是显而易见的)。
Dijkstra算法的数学证明:
由算法的数学描述,可知:
只有命题:“每次从QQ集中找到LkL_k最小的点vv,LvL_v即为从起点到vv的最短路径长度”正确时,算法正确。
可用广义数学归纳法证明,设起点为oo:- 证明:算法找到的第一个点为v1v_1,Lv1L_{v_1}即为从起点到v1v_1的最短路径长度。
用反证法:用反证法:
∵算法找到的第一个点一定是起点o最近的邻居\because 算法找到的第一个点一定是起点o最近的邻居
假设Lv1不是从起点到v1的最短路径长度假设L_{v_1}不是从起点到v_1的最短路径长度
则∃点v,使得∣∣ov→∣∣<∣∣ov1→∣∣,与已知矛盾则\exists点v,使得\left|\vec{ov}\right|
故假设不成立,子命题得证故假设不成立,子命题得证 - 证明:已用算法从QQ中依次找到了v1,v2,⋯,vkv_1,v_2,\cdots,v_k共kk个点,且Lv1,Lv2,⋯,LkL_{v_1},L_{v_2},\cdots,L_k是起点到各点的最短路径长度,则此时从QQ中依照算法再找一个点vk+1v_{k+1},Lk+1L_{k+1}即为起点到vk+1v_{k+1}的最短路径长度。
用反证法:用反证法:
假设Lk+1不是起点到vk+1最短路径的长度假设L_{k+1}不是起点到v_{k+1}最短路径的长度
所以设起点到vk+1的最短路径经过的点的集合为V,路径长度为L,则有L<Lk+1所以设起点到v_{k+1}的最短路径经过的点的集合为V,路径长度为L,则有L
∵由Lk的含义⇒V∩Q≠∅\because 由L_k的含义\Rightarrow V\cap Q\neq\emptyset
又∵o∈V且o∈S⇒V∩S≠∅又\because o\in V且o\in S \Rightarrow V \cap S \neq\emptyset
则设到vk+1的最短路径中,最靠近vk+1且不属于S的点为vx,vx的后继为vy则设到v_{k+1}的最短路径中,最靠近v_{k+1}且不属于S的点为v_x,v_x的后继为v_y
∵有向图中边均为正权边\because 有向图中边均为正权边
∴必有Lvx<Lvy⩽L,vy为vk+1时等号成立\therefore 必有L_{v_x}
又∵vx∉S⇒Lvx>Lk+1,产生矛盾又\because v_x\not\in S\Rightarrow L_{v_x}>L_{k+1},产生矛盾
故假设不成立,子命题得证故假设不成立,子命题得证
综上所述,该命题得证,故算法正确。
- 证明:算法找到的第一个点为v1v_1,Lv1L_{v_1}即为从起点到v1v_1的最短路径长度。
关于负权边
Dijkstra算法要求有向图中不得有负权边,如果图中有负权边,则在之前的证明过程中:
\because L_{v_x}
\therefore L_{v_x}>L_{k+1}不一定成立
故此时算法正确性不得证。
这里举一个简单的例子供读者自行对照理解:
时间复杂度
设有向图中,共有VV个顶点,EE条边。
传统Dijkstra算法中主要操作有:
- 每次从QQ集中找到LkL_k最小的点,最坏情况下需:
(V−1),(V−2),⋯,1
(V-1),(V-2),\cdots,1
次操作。
所以整个算法过程共需:(V−1)+(V−2)+⋯+1=(v−1)v2=V22−12(V-1)+(V-2)+\cdots+1=\frac{(v-1)v}{2}=\frac{V^2}{2}-\frac{1}{2}
次操作。 - 计算并更新各点邻居的LkL_k值,实质上是将所有的边遍历一遍,故需EE次操作。
则用大OO表示法:O(V22−12+E)⇔O(n2)O(\frac{V^2}{2}-\frac{1}{2}+E)\Leftrightarrow O(n^2)
故传统Dijkstra算法的时间复杂度为O(n2)O(n^2)
Dijkstra算法的优化
在上述对于传统Dijkstra算法的时间复杂度分析中,我们可知,(尤其是稀疏图中)从QQ集中找到LkL_k最小的点的过程极大影响了算法的性能,这个过程在顶点无序状态下需要O(n2)O(n^2)的复杂度。
因此对于顶点的有效排序可以大大地提高算法的性能,常用的方法有:小顶堆或优先队列:
- 小顶堆优化中,我们初始将所有顶点设置为一个小顶堆,此时堆顶一定为起点,而在每一次迭代中,我们将堆顶元素取出(复杂度O(1)O(1)),而后调整小顶堆(复杂度O(logn)O(logn)),这样调整VV次,直到堆中所有顶点全部加入SS。则整个算法的时间复杂度将从O(n2)O(n^2)优化为O(nlogn)O(nlogn)。
- 优先队列优化中,建立一个最小优先的优先级队列,队列中保存顶点和当前LkL_k值的二元组,初始将起点二元组入队,每当某个顶点的LkL_k值被更新,则将这个新的顶点二元组入队,每次迭代时,将队首元素取出并出队,直到队列为空。由于优先队列一般采用堆实现,故维护优先队列的复杂度同为O(logn)O(logn),则整个算法的时间复杂度同被优化为O(nlogn)O(nlogn)。
注意:优先队列优化中,新的顶点二元组入队时,旧的二元组依然在优先队列中,因此每次出队的元素可能会有杂音,如何识别并去除这些杂音是这种优化方式需要考虑的。
关于无向图
事实上,Dijkstra算法同样可以处理无向图中的单源最短路问题(无向图其实可看做一种特殊的有向图),但在这种情况下,要对算法做一些修改:标记已经访问过的边,在寻找邻居时不沿已标记过的边寻找。
关于空间复杂度
Dijkstra算法的空间复杂度视具体实现方法而定,采用邻接矩阵的存图方式的空间复杂度为O(n2)O(n^2),然而在稀疏图中,这种存储方式将有大量的空间浪费,因此推荐使用邻接表和有关标志数组存图。
算法具体代码实现多种多样,留作读者自行思考,在此不再赘述。
绝对原创,转载请注明出处。
才疏学浅,如有错误,望不吝赐教
详解Dijkstra算法(含数学证明和优化)相关推荐
- 最短路径——Dijkstra算法以及二叉堆优化(含证明)
一般最短路径算法习惯性的分为两种:单源最短路径算法和全顶点之间最短路径.前者是计算出从一个点出发,到达所有其余可到达顶点的距离.后者是计算出图中所有点之间的路径距离. 单源最短路径 Dijkstra算 ...
- ADMM,ISTA,FISTA算法步骤详解,MATLAB代码,求解LASSO优化问题
ADMM,ISTA,FISTA算法步骤详解,MATLAB代码,求解LASSO优化问题 原创文章!转载需注明来源:©️ Sylvan Ding's Blog ❤️ 实验目的 了解 ADMM, ISTA, ...
- 详解线性回归算法的纯Python实现
↑↑↑关注后"星标"简说Python人人都可以简单入门Python.爬虫.数据分析 简说Python推荐 来源|天池大数据科研平台作者|黄佳 零基础学机器学习--一文详解线性回归算 ...
- 详解floyd算法 及<MATLAB>实现
欢迎来到 < Haoh-Smile > 的博客,觉得受用客官就点个赞评论一下呗!** 详解floyd算法 及MATLAB实现 一.Floyd算法原理 Floyd算法是一个经典的动态规划算法 ...
- 区块链技术用解决拜占庭将军问题_两军问题_拜占庭将军问题详解图解算法
两军问题 我们来看一下好处理器的情况,但通信线路有问题.这就是所谓的两军问题,可以概括如下: A,B 两军师协同攻击敌军C, A和B在物理上是分开的,并使用信使进行通信. A向B发送一个消息" ...
- 详解rsync算法--如何减少同步文件时的网络传输量
详解rsync算法--如何减少同步文件时的网络传输量 先看下图中的场景,客户端A和B,以及服务器server都保存了同一个文件,最初,A.B和server上的文件内容都是相同的(记为File.1).某 ...
- 【算法知识】详解堆排序算法
点击蓝色字关注我们! 什么是堆 「堆」首先是一个完全二叉树,「堆」分为「大顶堆」和「小顶堆」: 「大顶堆」 : 每个节点的值大于或等于其左右孩子节点的值,称为大顶堆. 「小顶堆」同理就是每个节点的值小 ...
- 【算法知识】详解基数排序算法
已发布: [算法知识]详解选择冒泡算法 [算法知识]详解选择排序算法 [算法知识]详解插入排序算法 [算法知识]详解快速排序算法 [算法知识]详解归并排序算法 基本思想 基数排序的思想是将整数按位数切 ...
- 【算法知识】详解归并排序算法
已发布: [算法知识]详解选择冒泡算法 [算法知识]详解选择排序算法 [算法知识]详解插入排序算法 [算法知识]详解快速排序算法 基本思想 归并排序的基本思想是: 先将序列一次次分成子序列,直到子序列 ...
- 【算法知识】详解快速排序算法
基本思想 已发布: [算法知识]详解选择冒泡算法 [算法知识]详解选择排序算法 [算法知识]详解插入排序算法 本文的思路是以从小到大为例讲的. 快速排序的基本思想是任取待排序序列的一个元素作为中心元素 ...
最新文章
- Handler线程间通信
- OpenWRT(基于LEDE17.01.4)Open***的Client与Server端内网互通
- mysql创建只读权限用户_新品速递 | Harbor 修复权限提升漏洞,MySQL Plus 支持密码强度校验以及审计功能...
- java 流拆分_java - 在Java流中拆分字符串? - SO中文参考 - www.soinside.com
- 百度android输入法表情符号,百度输入法安卓5.8版推“史上最丰富颜文字库”
- android里build报错怎么办,Android Studio 当build时候出错解决办法
- 手把手教你训练一个秒杀科比的投篮AI,不服来练 | 附开源代码
- wing ide 3.x 中文设置
- Java 异常Exception e中e的getMessage()和toString()以及 e.printStackTrace();方法的区别
- faster rcnn fpn_Faster-RCNN详解和torchvision源码解读(三):特征提取
- IDEA+Java+SSM+Mysql+Bootstrap+Maven实现网上书城系统
- 送给大家一个很好的Web前端开发工具
- IDEA 各版本下载地址
- FMS3.5的安装使用
- linux 用户搬家后安装的软件,linux软件搬家
- 名家名言 Chuck Thacker
- 原生JS 简单购物车网页
- eclipse 项目中搜索资源(类方法,文件名,文件中的字符串)
- Cocos2d-x游戏暂停、继续游戏、重新开始界面的实现---之游戏开发《赵云要格斗》(10)
- 深入了解DataGridView控件