文章目录
本文参考UCAS卜东波老师算法设计与分析课程撰写
前言
在前面算法设计与分析:贪心算法(2)- 最短路问题(DP到贪心的优化)一文中,我们已经了解了如何从动态规划一步步向贪心进行优化,本质上贪心在动态规划的基础上添加了贪心选择性质,这让我们在处理最优化问题的时候,只需要考虑局部最优(短视)。
举个简单的例子,动态规划计算第n个子问题的时候,需要保证在第n-1个子问题最优,在这基础上再获得全局的最优解;而贪心则不需要,每一步我只需考虑当前最好的即可,就像排课问题中找最早下课的,或者最短路问题中,找已遍历点中相邻最近的。这都是短视,而这些成立的前提是,问题具有贪心选择性质!如何找到这个性质,其实就是我们前面几篇文章中的优化过程,还不太理解的可以点击文末上一篇或者目录查找。
本文,我们依据前面的基础,介绍Dijkstra算法
,这个算法在本科的时候,大家应该都耳熟能详,但本科学习的时候往往直接就给了这个算法实现过程,却没有讲述其设计的由来是通过Bellman Ford算法
一步步优化得来的,这一点其实挺重要的。
Dijkstra算法
Bellman Ford算法优化回顾
在介绍Dijkstra算法之前,先回顾一下前文Bellman Ford
优化的点,第一点是已遍历点中OPT值最小的无需计算,相当于去除冗余,因此我们不需要OPT这个二维数组了,用d(v)一维存储s到v的最短路径长度,d(v)代表的就是那个最小OPT值;第二点是只考虑已遍历点的相邻点,且从中找最近点,这个点的s的最短路径,我们前文已经证明过,是确定的,不会再变化了,也就是说用这个点来更新d数组。
Dijkstra算法描述
好的,下面我们有Dijkstra算法的伪代码:
为了更直观的理解,我绘制了一个样例如下:
其中,绿色箭头代表最短路径,PQ存储当前各个点的d(v)值,在每一轮循环中,我们要做两件事:
第一:找出所有未遍历点中d(v)值最小的,加入到已遍历点中;
第二:从所有已遍历点u(u是集合S中的点)出发,走一步,依据 min ( d ( v ) , d ( u ) + d ( u , v ) ) \min(d(v),d(u)+d(u,v)) min(d(v),d(u)+d(u,v))更新未遍历点v的d(v)值,每一个u都会更新一次d(v),可能不变,可能变小
具体的过程,图中已经十分详细,建议还不理解的手动画一个图(不要与我相同),自己将整个伪代码流程走一遍。
Dijkstra优化
上面已经详细描述了Dijkstra算法的实现原理,但有一部分没有说清楚,就是伪代码中第7行,当更新了d(v)之后,如何从中找到最小的d(v)?
最简单的思路就是一个for循环遍历PQ中的d(v)找出最小的,但这显示时间复杂度太高,结合外层我们需要遍历所有点,总的时间复杂度就是 O ( n 2 ) O(n^2) O(n2),一旦node过多,整个算法就很慢了。因此,我们需要在这点上面进行优化。
我们采用优先队列来存储d(v)值,即上图中的PQ集合(优先级就是node对应的d(v)值,后面统一用 k e y ( v ) key(v) key(v)),每次获取最小的值就从队列中获取,更新了 k e y ( v ) key(v) key(v)又添加到队列当中。先看一下伪代码:
这部分实际上利用了优先队列将上面的伪代码从教抽象的文字描述转换到较具体的伪代码了,仔细对比两个伪代码不难理解各个代码的意思。其中一开始的插入操作要执行n次,获取最小 k e y ( v ) key(v) key(v)在循环中要n次,更新队列中 k e y ( v ) key(v) key(v)要m次(m是边的数量)。
依据上面的分析,我们容易知道真正影响该算法时间复杂度的是队列的三个操作Insert
,ExtractMin
,DecreaseKey
,如果我们构成优先队列用的是普通的无序数组或链表,那么每次Insert
的复杂度就只有 O ( 1 ) O(1) O(1),但ExtractMin
就要 O ( n ) O(n) O(n)(每次都要遍历数组、链表),如果是有序数组或链表,Insert
需要 O ( n ) O(n) O(n)(遍历查找合适插入位置),ExtractMin
只用 O ( 1 ) O(1) O(1),因此,选择不同的数据结构构造队列,会导致算法时间复杂度不同,下表列出了四种实现队列的不同数据结构方式:
操作 | 线性链表(数组) | 二叉堆 | 二项堆* | 斐波那契堆* |
---|---|---|---|---|
Make(创建) | 1 | 1 | 1 | 1 |
Insert(插入):n次 | 1 | l o g n logn logn | l o g n logn logn | 1 |
ExtractMin(摘取最小):n次 | n | l o g n logn logn | l o g n logn logn | l o g n logn logn |
DecreasrKey(减小Key值,更新):m次 | 1 | l o g n logn logn | l o g n logn logn | 1 |
Delete(删除) | n | l o g n logn logn | l o g n logn logn | l o g n logn logn |
Union(合并) | 1 | n | l o g n logn logn | 1 |
FindMin(找最小) | n | 1 | l o g n logn logn | 1 |
Dijkstra | O ( n ∗ 1 + n ∗ n + m ∗ 1 ) = O ( n 2 ) O(n*1+n*n+m*1) = O(n^2) O(n∗1+n∗n+m∗1)=O(n2) | O ( n ∗ l o g n + n ∗ l o g n + m ∗ l o g n ) = O ( m l o g n ) O(n*logn+n*logn+m*logn)= O(mlogn) O(n∗logn+n∗logn+m∗logn)=O(mlogn) | O ( n ∗ l o g n + n ∗ l o g n + m ∗ l o g n ) = O ( m l o g n ) O(n*logn+n*logn+m*logn)= O(mlogn) O(n∗logn+n |