一、Dijkstra 算法 是什么?
是从一个顶点到其余各顶点的最短路径算法。
解决的是有权图中最短路径问题。
迪杰斯特拉算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止。
1.特点
Dijkstra 不支持有负权值边的存在。
算法时间复杂度:O(ElogV) 其中E为边数、V为节点数。
算法的核心其实是relaxation,松弛操作,这对于有权图来说,十分重要。
因为两个节点中的最短路径,在有权途中,绕一圈反而比直接连接更优。
1.思想
a. 遍历节点所连接的节点,找到没有访问的节点中,能够以最短的方式到达的点,那么从源点到这个节点的最短路径就是这个路径。将其标记(下图1 节点 2 )。
b. 然后开始relaxation:遍历2的临边,发现2+1<5;即更新0->1的路径长度(图2 节点1、3).
c. 重复a.操作,确定了0->1 的最短路径为3,并进行标记。(因为这个边最小,从其他节点绕一圈肯定比这个路径要长。)然后根据节点1的临边,进行b.的操作,更新4节点权: 4(从0-1已经确定是3,3到4的权值为1,所以更新0-4路径权值为4)
d. 最终得到以0为根的最短路径树。 § 实现细节:在列表中没有访问的节点种中,找到最短路径的节点;更新列表;
复杂度分析:
用最小索引堆,开节点V个数个空间。
所以每次插入、更新都是logV 的复杂度,然后遍历所有边是E。 所以最终的时间复杂度是O(ElogV)。
二、简易实现
bobo老师的实现代码解读::
//代码涉及:(文末参考中由相关简介)
模板类、模板函数
最小索引堆
1.解读
算法初始化过程:
distTo 为存储从source到各个节点的最短路径。初始化为节点个数个空数组。用模板类型的默认构造来初始化。
marked 为标记当前节点是否遍历过了。初始化为节点个数个的空数组。
from 为了记录最短路径中,当前节点是从哪个边过来的。可以恢复整条路经出来。
this->s = s;
distTo = new Weight[G.V()];
marked = new bool[G.V()];
for( int i = 0 ; i < G.V() ; i ++ ){
distTo[i] = Weight();
marked[i] = false;
from.push_back(NULL);
}
Dijkstra 实现过程:
- 对起始点进行初始化,标记source 到source 的最短路径0 、相应的边0 、并将这个边插入最小堆中。
- 提取最小堆中,最小权值的索引。将标定为新进入最短路径的节点;(上述一、思想a、中有讲到贪心的过程)
- 开始遍历当前节点v的临边e。如果当前节点v的临边e的另外一个节点w还没有被确认找到最短路径。
- 如果这个w节点没有访问过,或者这个w节点访问过,但是经过当前节点v再到节点w的距离更短(relaxation ),就更新这个途径节点v的最短路径到distTo;并更新当前节点最短路径是经过哪个节点的,到from。
- 继续循环2.提取最小权值的索引。知道最小堆被取光了。
从步骤2.开始就已经是relaxation 松弛操作了,就是经过当前节点,还能不能使得当前节点的临边节点的最短路径缩短。(如上图一、思想中图所示,通过遍历节点2 的临边节点,发现经过节点2,再到节点1的距离为2+1 = 3,小于之前从0直接到1节点的距离5 )
通过这样的松弛操作,使得临边节点的最短路径慢慢更新到最短。最终贪心的认为,更新完的最短路径中最短的那条,可以确认是最短路径中的一条路径了。
同样,relaxation 针对的只是没有找到最短路径的节点。其中包含了还没有访问过的节点,和访问过的节点,但是通过当前节点绕到这个节点的距离更短的节点。(如上图中的节点1,就是通过节点2绕行后,距离更短)
// 使用索引堆记录当前找到的到达每个顶点的最短距离
IndexMinHeap<Weight> ipq(G.V());
// 对于其实点s进行初始化
distTo[s] = Weight();
from[s] = new Edge<Weight>(s, s, Weight());
ipq.insert(s, distTo[s] );
marked[s] = true;
while( !ipq.isEmpty() ){
int v = ipq.extractMinIndex();
// distTo[v]就是s到v的最短距离
marked[v] = true;
// 对v的所有相邻节点进行更新
typename Graph::adjIterator adj(G, v);
for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() ){
int w = e->other(v);
// 如果从s点到w点的最短路径还没有找到
if( !marked[w] ){
// 如果w点以前没有访问过,
// 或者访问过, 但是通过当前的v点到w点距离更短, 则进行更新
if( from[w] == NULL || distTo[v] + e->wt() < distTo[w] ){
distTo[w] = distTo[v] + e->wt();
from[w] = e;
if( ipq.contain(w) )
ipq.change(w, distTo[w] );
else
ipq.insert(w, distTo[w] );
}
}
}
}
2.完整代码
来自bobo老师github,文末有连接。
// Dijkstra算法求最短路径
template<typename Graph, typename Weight>
class Dijkstra{
private:
Graph &G; // 图的引用
int s; // 起始点
Weight *distTo; // distTo[i]存储从起始点s到i的最短路径长度
bool *marked; // 标记数组, 在算法运行过程中标记节点i是否被访问
vector<Edge<Weight>*> from; // from[i]记录最短路径中, 到达i点的边是哪一条
// 可以用来恢复整个最短路径
public:
// 构造函数, 使用Dijkstra算法求最短路径
Dijkstra(Graph &graph, int s):G(graph){
// 算法初始化
assert( s >= 0 && s < G.V() );
this->s = s;
distTo = new Weight[G.V()];
marked = new bool[G.V()];
for( int i = 0 ; i < G.V() ; i ++ ){
distTo[i] = Weight();
marked[i] = false;
from.push_back(NULL);
}
// 使用索引堆记录当前找到的到达每个顶点的最短距离
IndexMinHeap<Weight> ipq(G.V());
// 对于其实点s进行初始化
distTo[s] = Weight();
from[s] = new Edge<Weight>(s, s, Weight());
ipq.insert(s, distTo[s] );
marked[s] = true;
while( !ipq.isEmpty() ){
int v = ipq.extractMinIndex();
// distTo[v]就是s到v的最短距离
marked[v] = true;
// 对v的所有相邻节点进行更新
typename Graph::adjIterator adj(G, v);
for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() ){
int w = e->other(v);
// 如果从s点到w点的最短路径还没有找到
if( !marked[w] ){
// 如果w点以前没有访问过,
// 或者访问过, 但是通过当前的v点到w点距离更短, 则进行更新
if( from[w] == NULL || distTo[v] + e->wt() < distTo[w] ){
distTo[w] = distTo[v] + e->wt();
from[w] = e;
if( ipq.contain(w) )
ipq.change(w, distTo[w] );
else
ipq.insert(w, distTo[w] );
}
}
}
}
}
// 析构函数
~Dijkstra(){
delete[] distTo;
delete[] marked;
delete from[0];
}
// 返回从s点到w点的最短路径长度
Weight shortestPathTo( int w ){
assert( w >= 0 && w < G.V() );
assert( hasPathTo(w) );
return distTo[w];
}
// 判断从s点到w点是否联通
bool hasPathTo( int w ){
assert( w >= 0 && w < G.V() );
return marked[w];
}
// 寻找从s到w的最短路径, 将整个路径经过的边存放在vec中
void shortestPath( int w, vector<Edge<Weight>> &vec ){
assert( w >= 0 && w < G.V() );
assert( hasPathTo(w) );
// 通过from数组逆向查找到从s到w的路径, 存放到栈中
stack<Edge<Weight>*> s;
Edge<Weight> *e = from[w];
while( e->v() != this->s ){
s.push(e);
e = from[e->v()];
}
s.push(e);
// 从栈中依次取出元素, 获得顺序的从s到w的路径
while( !s.empty() ){
e = s.top();
vec.push_back( *e );
s.pop();
}
}
// 打印出从s点到w点的路径
void showPath(int w){
assert( w >= 0 && w < G.V() );
assert( hasPathTo(w) );
vector<Edge<Weight>> vec;
shortestPath(w, vec);
for( int i = 0 ; i < vec.size() ; i ++ ){
cout<<vec[i].v()<<" -> ";
if( i == vec.size()-1 )
cout<<vec[i].w()<<endl;
}
}
};
参考
模板类设计
[最小堆]
[最小索引堆]
bobo老师课程
bobo老师github