这篇文章用来复习最短路径问题之单源最短路径问题.
解决这个问题的算法是Dijkstra算法,他是以一个人名来命名的,老爷子是个荷兰人
可惜的是他在02年的时候去世了
他生前提出过许多很有意思的算法问题并引发了一系列的思考,包括哲学家聚餐问题,信号量PV操作等.
图灵奖当然少不了他啦.
使用Dijkstra解决单源最短路问题
有着近40年历史的解法是贪婪算法的最好例子,贪婪算法一般的分阶段求解一个问题,在每个阶段它都把当前出现的当作是最好的去处理.
下面用一个案例讲解一下算法流程
假定V1是被设置为了起点
第一步初始化,从V1点开始更新与其相邻的顶点距离.
V | known | dv | p v |
---|---|---|---|
V1 | 1 | 0 | -1 |
V2 | 0 | 2 | 1 |
V3 | 0 | MAXDIX | -1 |
V4 | 0 | 1 | 1 |
V5 | 0 | MAXDIX | -1 |
V6 | 0 | MAXDIX | -1 |
V7 | 0 | MAXDIX | -1 |
解释一下列属性名称的含义:
- V :顶点编号
- known : 顶点访问状态,1为被访问 , 0为未被访问
- dv : 到此顶点的距离
- pv : 记录到达此顶点所经过的路径,存放的是编号
算法的流程:
在未被访问过的顶点中找到dv最小的的顶点Vm.即Vm的dv
之后遍历Vm的所有未被访问过的相邻点Vi,判断是否需要更新起点到达Vm的距离.
如果 d(起点,Vi) > d(起点,Vm)+ W(Vm,Vi)
d(起点,Vi) = d(起点,Vm)+ W(Vm,Vi)
直到找不到Vm为止.W是指的权重.
所以,下一个要被遍历的点是V4,因为它的dv是1.
发现V4连接着V3/5/6/7,又都是未访问的状态.
于是判断 d(起点,V3/5/6/7) > d(起点,V4)+ W(V4,V3//5/6/7)
由于在初始化的时候d(起点,V3/5/6/7)都是MAXDIX,以上不等式必定成立.
故这四个顶点都会被更新;
遍历完成,更新后数据如下.
V | known | dv | p v |
---|---|---|---|
V1 | 1 | 0 | -1 |
V2 | 0 | 2 | 4 |
V3 | 0 | 3 | 1 |
V4 | 1 | 1 | 1 |
V5 | 0 | 3 | 4 |
V6 | 0 | 9 | 4 |
V7 | 0 | 5 | 4 |
接下来再在未访问的顶点中找dv最小的顶点,即V2
它相邻着V4和V5但是只能去判断V5,因为V4已经访问过了.
在V5上判断 是否 d(起点,V5) > d(起点,V2)+ W(V2,V5)即判断 3 > 2 + 10 ,显然不成立.
遍历完成.
故此次V2的访问并没有有效的更新图中的数据.
V | known | dv | p v |
---|---|---|---|
V1 | 1 | 0 | -1 |
V2 | 1 | 2 | 1 |
V3 | 0 | 3 | 4 |
V4 | 1 | 1 | 1 |
V5 | 0 | 3 | 4 |
V6 | 0 | 9 | 4 |
V7 | 0 | 5 | 4 |
又开始寻找,找到了V3和V5都是符合要求,那到底选哪个呢,无所谓.
我们按照谁先被被找到就被选取的规则,选择V3访问.
更新了起点到达V6的值,即V6的dv.
再次寻找,选择V5访问.
V5连接着V7,尝试更新V7的dv,发现不符合更新规则(5 !> 3 + 6)
故选择V3,V5访问后的结果如下
V | known | dv | p v |
---|---|---|---|
V1 | 1 | 0 | -1 |
V2 | 1 | 2 | 1 |
V3 | 1 | 3 | 4 |
V4 | 1 | 1 | 1 |
V5 | 1 | 3 | 4 |
V6 | 0 | 8 | 3 |
V7 | 0 | 5 | 4 |
最后选择V7访问,根据规则更新了V6的dv,最后选择V6访问,没找到可访问的点.
算法结束;
这是最后的结果
V | known | dv | p v |
---|---|---|---|
V1 | 1 | 0 | -1 |
V2 | 1 | 2 | 1 |
V3 | 1 | 3 | 4 |
V4 | 1 | 1 | 1 |
V5 | 1 | 3 | 4 |
V6 | 1 | 6 | 7 |
V7 | 1 | 5 | 4 |
关于初始化:
- dv的初始化要为一个最大值.
- pv的初始化要为一个不存在的编号,如-1;
// 初始化顶点编号
// 初始化顶点访问状态
// 初始化记录路径数组
// 初始化距离数组
for (int i = 0;i < vertexNum;i++)
{
vertexPtr[i].verIndex = i;
visited[i] = NOVISIT;
path[i] = -1;
distance[i] = MAXDIS;
}
关于路径的记录:
当在判断结果为需要更新后path[Vi] = Vm
那我代码中的例子举例如下:
// 这就是Vm
int minDistanceVertex;
// 寻找权值最小且未被访问的顶点编号
while ((minDistanceVertex = findMinDistanceVertex()) != -1)
{
// 设置状态被访问
setVertexState(minDistanceVertex,VISITED);
// 开始遍历相邻点
Node_s * node = vertexPtr[minDistanceVertex].NEXT;
while (node)
{
// 若此相邻点未被访问
if (getVertexState(node->verIndex) == NOVISIT)
{
// 计算更新后的距离
int updateDis = distance[minDistanceVertex] + node->weight;
// 判断更新后的距离是否小于现有距离
if (updateDis < distance[node->verIndex])
{
// 更新现有距离
distance[node->verIndex] = updateDis;
// 记录当前顶点的上一个顶点编号
// path[Vi] = Vm
path[node->verIndex] = minDistanceVertex;
}
}
node = node->NEXT;
}
}
关于Dijkstra:
适用于权值为非负图的单源最短路径,整个算法过程将花费O(V2)时间查找最小值,每次更新dv的时间是常数,而每条边最多有一次被更新,总计为O(E),因此总的运行时间为O(E+V2)=O(V2),若图是稠密,边数E=⊙(V2),其总结果只是可以忽略的线性增长,可见,算法不仅简单,而且基本上最优,就是因为它的运行时间与边数成线性关系.若图是稀疏的,边数E=⊙(V),那么就太慢了,在这种情况下,dv需要存储在优先队列中,最佳的是使用斐波那契堆,使用这种数据结构的运行时间是O(E+V log(V)),具有良好的理论时间界,但是它需要相等数量的系统开销.
使用场景:
- 在一些诸如计算机邮件和大型公交传输的典型问题中,大多数顶点只有几条边,故在许多应用中使用优先队列来解决这种问题是很重要的,目前,尚不清楚在实践中是否使用斐波那契堆比使用具有二叉堆的Dijkstra算法更好.所以这种问题没有一个平均情形时间结果.
其他:
- 在我使用了邻接表存储图并且没有使用优先队列遍历的实现中,我需要的变量:
// 记录距离的数组
int * distance;
// 记录路径的数组
int * path;
// 记录顶点访问状态
VISITEDSTATE * visited;
总结:
学习了贪婪算法的思想
代码中接口部分和前面复习的图的遍历基本保持一致.
详细请见我的github.