基于STL优先队列和邻接表的dijkstra算法

首先说下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
posted @ 2017-04-02 05:59  Harley_Quinn  阅读(1590)  评论(2编辑  收藏  举报