最近学习《数据结构与算法分析》一书,在“图论算法”一章再次接触到了Dijkstra算法,之前没有深入理解,这里对其原理进行梳理。
Dijkstra算法求解的是“单源最短路径问题”,是“贪婪算法”的一个很好的应用,按照我的理解就是“无权最短路径的类拓扑排序算法”+“调整策略”。而此处的调整策略就是“贪婪算法”的核心。
下面简要介绍涉及的相关概念:
1.贪婪算法:简单地说就是程序运行的每一个当前时间点,都会选择对当前最有利的情况,如果之前的决策导致的结果没有现在的好,那就重新调整为当前的决策。(前提是当前时刻做出的决策与未来时刻无关,也就是满足“马尔可夫”过程)对应于Dijkstra算法,后续会提及具体体现的地方。
例子:购物找零钱时,商家为了省事,总会用尽量少的硬币给你,比如商家要找你五毛钱,那么总会用一个5毛硬币而不用五个1毛硬币,这里商家没有考虑后续顾客找零钱的情况。
2.拓扑排序:有向图中如果节点v指向w,那么按照拓扑排序后,w总出现在v后面。
一般解法是使用队列或栈的数据结构,按照一定的条件节点被划分为几个组,按顺序同一组的节点进入一个队列,此时其余组节点不进入此队列,队列中的节点处理完后出队,并在后续操作中不再涉及,只有当队列中所有节点处理完了,下一组节点才能进入该队列。
3.无权最短路径算法:一般采用“广度优先搜索”算法,结合下表分析
v | known | dv | pv |
---|---|---|---|
v1 | F | ∞ | 0 |
v2 | F | ∞ | 0 |
v3 | F | 0 | 0 |
v4 | F | ∞ | 0 |
v5 | F | ∞ | 0 |
这里v表示图中的节点,known表示该节点是否处理完(是否出列队,F表示未处理,T表示处理过了,当该节点被置为known后就要处理他的邻接点),*dv*表示该节点到出发点的距离,*pv*记录路径。
广度优先:首先选择出发点v3,计算离出发点距离=0,保留路径。处理完后将其known置为T(该节点在后续的程序中不再涉及);之后搜索该点的邻接点,记录它们到该点的距离并在表格中更新;随后对于邻接点按照其到上一个节点的距离从近到远处理,处理过程与上述过程一致,直到所有节点均被处理完,也就是表格中“known”列均为T。最终可以从表格""*pv*列得到最短路径。
4.Dijkstra算法:就是无权图最短路径算法中每条边的长度不再为1,代表的是两点间的距离。同样可以使用上述广度优先算法。
一般处理过程:
1)选取出发点计算其路径长(出发点为0),然后对其置known;
2)调整该节点的邻接点(也就是计算邻接点的长度);
3)处理邻接点中长度最小的节点,将其置known,并计算其邻接点长度;
4)按长度从小到大依次处理剩余邻接点,依次标记为known,并计算它们各自的邻接点长度,如果计算结果小于之前的长度,则更新为当前时刻更小的长度(也就是“贪婪”的体现,说明当前路径可以有更好的结果),如果其邻接点在前述已被置为known,则跳过该节点。
5)重复执行过程3)和4),直到表格中最后一个长度最大的节点被置为known。
结合下表与图进行说明:
算法的初始配置
v | known | dv | pv |
---|---|---|---|
v1 | F | 0 | 0 |
v2 | F | ∞ | 0 |
v3 | F | ∞ | 0 |
v4 | F | ∞ | 0 |
v5 | F | ∞ | 0 |
v6 | F | ∞ | 0 |
v7 | F | ∞ | 0 |
以v1作为出发点:
v1被置为known后
v | known | dv | pv |
---|---|---|---|
v1 | T | 0 | 0 |
v2 | F | 2 | v1 |
v3 | F | ∞ | 0 |
v4 | F | 1 | v1 |
v5 | F | ∞ | 0 |
v6 | F | ∞ | 0 |
v7 | F | ∞ | 0 |
v4被置为known后
v | known | dv | pv |
---|---|---|---|
v1 | T | 0 | 0 |
v2 | F | 2 | v1 |
v3 | F | 3 | v4 |
v4 | T | 1 | v1 |
v5 | F | 3 | v4 |
v6 | F | 9 | v4 |
v7 | F | 5 | v4 |
v2被置为known后
v | known | dv | pv |
---|---|---|---|
v1 | T | 0 | 0 |
v2 | T | 2 | v1 |
v3 | F | 3 | v4 |
v4 | T | 1 | v1 |
v5 | F | 3 | v4 |
v6 | F | 9 | v4 |
v7 | F | 5 | v4 |
v5被置为known后
v | known | dv | pv |
---|---|---|---|
v1 | T | 0 | 0 |
v2 | T | 2 | v1 |
v3 | T | 3 | v4 |
v4 | T | 1 | v1 |
v5 | T | 3 | v4 |
v6 | F | 8 | v3 |
v7 | F | 5 | v4 |
v7被置为known后
v | known | dv | pv |
---|---|---|---|
v1 | T | 0 | 0 |
v2 | T | 2 | v1 |
v3 | T | 3 | v4 |
v4 | T | 1 | v1 |
v5 | T | 3 | v4 |
v6 | F | 6 | v7 |
v7 | T | 5 | v4 |
v6被置为known后算法终止
v | known | dv | pv |
---|---|---|---|
v1 | T | 0 | 0 |
v2 | T | 2 | v1 |
v3 | T | 3 | v4 |
v4 | T | 1 | v1 |
v5 | T | 3 | v4 |
v6 | T | 6 | v7 |
v7 | T | 5 | v4 |
算法的伪代码如下:
void dijkstra(Vertex s)
{
for each Vertex v // 初始化各节点
{
v.dist = INFINITY;
v.known = false;
}
s.dist = 0; // 初始节点长度为0
while(there is an unknown distance vertex) // 存在未处理的节点
{
Vertex v = smallest unknown distance vertex; // 每次队列里仅含有同一组节点,选择最短的节点
v.known = true; // 节点置known
for each Vertex w adjacent to v // 节点的每一个邻接点
if(!w.known) // 如果邻接点之前未被处理过则执行以下过程
{
DistType cvw = cost of edge from v to w; // 计算邻接点的长度
if(v.dist + cvw < w.dist) // 如果当前时刻的节点距离小于之前计算的结果,则进行更新(贪婪算法)
{
// Update w
decrease(w.dist to v.dist + cvw); // 更新节点的长度为更小的值
w.path =v; // 更新节点的路径
}
}
}
}
参考书籍《数据结构与算法分析-Java语言描述》