首先说下STL优先队列的局限性,那就是只提供入队、出队、取得队首元素的值的功能,而dijkstra算法的堆优化需要能够随机访问队列中某个节点(来更新源点节点的最短距离)。
看似可以用vector配合make_heap/push_heap/pop_heap来实现这个功能,实际上手动实现就会发现问题所在。比如在dist[v] > dist[u] + cost(u,v)时,需要更新dist[v],然后重新确定v在vector的位置,需要使用push_heap,这样问题就出现了。
v又在vector的哪个位置呢?只有在vector中一个个查找,除非在之前维护最小(距离)堆的时候,每次交换元素,记录元素的位置变化,也就是用int pos[V];(V为顶点数,下面不再重复说明)来记录,每次push_heap和pop_heap使堆的元素交换的时候(swap(heap[i], heap[j];)还要顺便交换位置(swap(pos[i], pos[j]);)
而仅仅是用STL提供的接口是无法实现的,只有从头造轮子。
于是有个折中的方法,那就是仍然使用优先队列。只是在更新点v的最短距离时,把点v重新加入队列中,而队列中已经存在的v无法访问就继续搁着。
也就是说队列中有2个点v,一个是用更新后的距离进行堆操作的,一个是用更新前的距离进行堆操作的。
首先我不是用while (!q.empty())判断终止条件的,而是照着书上的for (int i = 0; i < V; i++)判断,这样问题就在于,可能点v已经出队了(代表着已经确定源点到点v的最短路径),此时若点v出队则需要跳过。
书上之所以只循环V-1次是因为书上用的堆优化,不会像我这样重复添加某元素到堆中,而是更新堆中元素的权值并移动位置。由于每次循环都能确定源点到某个点的最短路径,所以只需要V-1次足矣。
而退而求其次的直接用优先队列的做法也可以直接循环V-1次,只不过每次循环开头要判断队首元素是否已经确定了最短距离,若是则弹出,一直到队首元素是未确定最短距离。不如while (!q.empty())加continue简洁(见下面核心代码)
auto comp = [](int v1, int v2) { return dist[v1] > dist[v2]; }; priority_queue<int, vector<int>, decltype(comp)> q(comp); dist[v0] = 0; q.push(v0); while (!q.empty()) { int u = q.top(); q.pop(); if (vis[u]) // 已经求过v0到u的最短路径 continue; vis[u] = true; for (auto& e : adjList[u]) { int v = e.adjID; if (!vis[v] && dist[v] - dist[u] > e.len) { dist[v] = dist[u] + e.len; pre[v] = u; q.push(v); } } }
其他代码就不贴了,对其中用到的一些全局变量做个说明。
注意如果dist是定义在dijkstra函数体内的,lambda表达式要捕获dist的引用,即auto comp = [&dist](后面不变)
vector<AdjList> adjList; // 邻接表, 预先读取了数据 // AdjList是STL容器Container<T>的别名(Container可以是vector或list或deque),T是AdjEdge(邻接边), 定义如下(省略了构造函数) struct AdjEdge { int adjID; // 邻接点的ID int len; // 邻接边的长度 }; vector<int> dist(V, INT_MAX); // 最短距离 vector<int> pre(V, -1); // 最短路径上的前1个节点号 deque<bool> vis(V, false); // 若求出了最短距离则置为true