求解单源最短路的Dijkstra算法
Dijkstra算法1,英文为Dijkstra’s algorithm,常被译为迪杰斯特拉算法。该算法由Edsger Wybe Dijkstra2在1956年发现。Dijkstra算法使用类似BFS的方法解决带权无负权图的单源最短路问题。
算法描述
解决单源最短路,便是需要求解图 G G G上某个源点 s s s到其它点 v ∈ V − { s } v\in V- \lbrace s \rbrace v∈V−{s}的权值和最小的路径,这里显然有两个重要求解目标——权值和、路径,这里我们先不考虑具体路径的求解。
解集dist初始化
Dijkstra算法在过程中动态维护解集
d
i
s
t
dist
dist,其中
d
i
s
t
[
i
]
dist[i]
dist[i]为当前
s
s
s到编号为
i
i
i的点之间的最小权值和。显然在算法开始前,有
d
i
s
t
[
s
]
=
0
d
i
s
t
[
v
]
=
∞
dist[s]=0 \\ dist[v]=\infty
dist[s]=0dist[v]=∞
松弛
Dijkstra的基础操作是松弛。显然对于任意时刻,如果存在一条边 w ( u , v ) w(u,v) w(u,v),使得当前 d i s t [ u ] + w e i g h t [ u ] [ v ] < d i s t [ v ] dist[u]+weight[u][v]<dist[v] dist[u]+weight[u][v]<dist[v],则可利用该边,将之前的解路径 p a t h [ v ] path[v] path[v]优化为 p a t h [ u ] → w ( u , v ) path[u]\rightarrow w(u,v) path[u]→w(u,v)。
贪心地进行松弛操作
算法维护两个顶点集合 S S S和 Q Q Q。集合 S S S保留所有已知实际最短路径值的顶点,而集合 Q Q Q则保留其他所有顶点。集合 S S S初始状态为空,而后每一步都有一个顶点从 Q Q Q移动到 S S S。这个被选择的顶点是 Q Q Q中拥有最小的 d i s t dist dist值的顶点。当一个顶点 u u u从 Q Q Q中转移到了 S S S中,算法对 u u u的每条邻边 w ( u , v ) w(u,v) w(u,v)进行松弛。
由于初始时 d i s t [ s ] dist[s] dist[s]为 0 0 0最小,故第一轮中的松弛其实是将 d i s t [ v ] dist[v] dist[v]初始化为 w e i g h t [ s ] [ v ] weight[s][v] weight[s][v],整个算法将进行 n n n轮松弛。也有说法将第一轮松弛作为初始化的一部分,认为 n − 1 n-1 n−1轮松弛即可得到结果。这是亟需辨析的点。
伪代码
《算法导论》给出了以下伪代码:
求解单源单汇点
上述流程中,一旦对汇点
t
t
t的松弛完成,则意味着最终的
d
i
s
t
[
t
]
dist[t]
dist[t]已经求得,此时可以终止算法。但该操作并不会降低算法的渐进复杂度,因为该汇点可能在最后一个才被选中进行松弛。
时间复杂度
我们发现整个算法的复杂度主要由顶点数和 E X T R A C T − M I N EXTRACT-MIN EXTRACT−MIN决定。在朴素的Dijkstra算法中,我们很容易想到枚举 d i s t i dist_i disti打擂找出每轮的最小值,这一操作的复杂度为 O ( ∣ V ∣ ) O(|V|) O(∣V∣),也就是整个算法的复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。
事实上,因为我们无差别地将当前顶点集 Q Q Q首的邻点丢进 Q Q Q,所以算法实际上遍历了每一条边,因此可以更准确地将复杂度写为 O ( ∣ V ∣ 2 + ∣ E ∣ ) O(|V|^2+|E|) O(∣V∣2+∣E∣)
但打擂也太蠢了。我们完全可以使用堆来优化 E X T R A C T − M I N EXTRACT-MIN EXTRACT−MIN过程,最方便的是使用STL中的priority_queue,这种情况下 E X T R A C T − M I N EXTRACT-MIN EXTRACT−MIN的复杂度降到了 O ( log ∣ V ∣ ) O(\log |V|) O(log∣V∣),整体复杂度为 O ( ( ∣ E ∣ + ∣ V ∣ ) log ∣ V ∣ ) O((|E|+|V|)\log |V|) O((∣E∣+∣V∣)log∣V∣)。
如果使用斐波纳契堆实现优先队列,还可以将算法复杂度转为 O ( ∣ E ∣ + ∣ V ∣ log ∣ V ∣ ) O(|E|+|V|\log |V|) O(∣E∣+∣V∣log∣V∣),但当图为稠密图时,常数可能过大,并没有取得显著的效率提升。
正确性证明
我们考虑用数学归纳法来证明Dijkstra算法的正确性,这里仅用简单的言语描述推理过程,如有兴趣了解更多,可参见算法导论或Dijkstra本人的证明,这些内容都可以在WikiPedia3上找到。
- 源点入队
- 取队首,然后松弛,实际上也就是令 d i s t i = w e i g h t [ s ] [ i ] dist_i=weight[s][i] disti=weight[s][i],将每次松弛产生的 d i s t dist dist插入队列。
- 取队首,此时取出的实际上就是与 s s s直接相连的最近的点,我们且称之为 v 1 v_1 v1,我们可以证明此时 d i s t v 1 dist_{v_1} distv1已最小,原因是如果 d i s t v 1 dist_{v_1} distv1未达最小,我们就可以通过松弛操作优化它,但松弛的前提是存在点 k k k使得 d i s t [ k ] + w e i g h t [ k ] [ v 1 ] < d i s t [ v 1 ] dist[k]+weight[k][v_1]<dist[v_1] dist[k]+weight[k][v1]<dist[v1],显然,如果存在该松弛,则有 d i s t [ k ] < d i s t [ v 1 ] dist[k]<dist[v_1] dist[k]<dist[v1],这与当前取出队首为 d i s t dist dist最小点矛盾。利用 v 1 v_1 v1松弛其邻点,同样,将更新后的 d i s t dist dist插入优先队列。
- 同3理,可推得每一步取出的队首都应该无法再被松弛,其 d i s t dist dist已达最小值(这也是前面说到可以优化单汇求解过程的原理)。
参考实现
struct HeapNode {
int d, u;
bool operator < (const HeapNode& rhs) const {
return d > rhs.d;
}
};
struct Dijkstra {
int n, m;
vector<Edge> edges;
vector<int> G[maxn];
bool done[maxn]; //是否已永久标号
int d[maxn]; //s到各个点的距离
int p[maxn]; //最短路中的上一条弧
void init(int n) {
this->n = n;
for (int i = 0; i < n; i++) G[i].clear();
edges.clear();
}
void AddEdge(int from, int to, int dist) {
edges.push_back(Edge(from, to, dist));
m = edges.size();
G[from].push_back(m - 1);
}
void dijkstra(int s) {
priority_queue<HeapNode> Q;
for (int i = 0; i < n; i++) d[i] = INF;
d[s] = 0;
memset(done, 0, sizeof(done));
Q.push((HeapNode) {0, s});
while (!Q.empty()) {
HeapNode x = Q.top();
Q.pop();
int u = x.u;
if (done[u]) continue;
done[u] = true;
for (int i = 0; i < G[u].size(); i++) {
Edge &e = edges[G[u][i]];
if (d[e.to] > d[u] + e.dist) {
d[e.to] = d[u] + e.dist;
p[e.to] = G[u][i];
Q.push((HeapNode) {d[e.to], e.to});
}
}
}
}
};