【问题标题】:Dijkstra shortest path algorith performance of std::priority_queue Vs std::setstd::priority_queue 与 std::set 的 Dijkstra 最短路径算法性能
【发布时间】:2017-09-12 01:36:19
【问题描述】:

我想了解这些容器在时间复杂度方面的主要区别。 我已经尝试了 3 种 Dijkstra 算法的实现,如下所述:

1- 用一个简单的数组作为队列

2- 使用 STL priority_queue

3- 设置了 STL

我测试过的图很大,它包含超过 150000 个顶点,有方向,所有边的权重都是正的。

我得到的结果如下:

1 - 使用数组的算法非常慢 --> 这是预期的

2 - 使用 STL priority_queue,算法运行速度比数组快得多 --> 这也是预期的

3 - 使用 STL 设置算法运行得非常快,我说的是比 priority_queue 快 100 倍 --> 我没想到会看到如此巨大的性能......

知道 std::priority_queue 和 std::set 是存储元素的数据容器,并且两者具有基本相同的插入复杂度 O(log n),我不明白它们之间存在这么大的性能差异。你对此有什么解释吗?

感谢您的帮助,

已编辑: 这是我的实现的摘要:

与 std::set:

unsigned int Graphe::dijkstra(size_t p_source, size_t p_destination) const {

....


set<pair<int, size_t>> set_vertices;


vector<unsigned int> distance(listAdj.size(),
        numeric_limits<unsigned int>::max());


vector < size_t
        > predecessor(listAdj.size(),
                numeric_limits < size_t > ::max());


distance[p_source] = 0;
set_vertices.insert( { 0, p_source });


while (!set_vertices.empty()) {

    unsigned int u = set_vertices.begin()->second;


    if (u == p_destination) {
        break;
    }

    set_vertices.erase( { distance[u],
            u });


    for (auto itr = listAdj[u].begin();
            itr != listAdj[u].end(); ++itr) {


        int v = itr->destination;
        int weigth = itr->weigth;


        if (distance[v]
                > distance[u] + weigth) {


            if (distance[v]
                    != numeric_limits<unsigned int>::max()) {
                set_vertices.erase(
                        set_vertices.find(
                                make_pair(distance[v],
                                        v)));
            }

            distance[v] = distance[u] + weigth;

            set_vertices.insert( { distance[v],
                    v });

            predecessor[v] = u;
        }
    }
}

....

return distance[p_destination];}

并带有priority_queue:

unsigned int Graphe::dijkstra(size_t p_source, size_t p_destination) const {

...

typedef pair<size_t, int> newpair;

priority_queue<newpair, vector<newpair>, greater<newpair> > PQ;

vector<unsigned int> distance(listAdj.size(),
        numeric_limits<unsigned int>::max());


vector < size_t
        > predecessor(listAdj.size(),
                numeric_limits < size_t > ::max());


distance[p_source] = 0;
PQ.push(make_pair(p_source, 0));

while (!PQ.empty()) {


    unsigned int u = PQ.top().first;


    if (u == p_destination) {
        break;
    }

    PQ.pop();


    for (auto itr = listAdj[u].begin();
            itr != listAdj[u].end(); ++itr) {


        int v = itr->destination;
        int weigth = itr->weigth;


        if (distance[v]
                > distance[u] + weigth) {

            distance[v] = distance[u] + weigth;

            PQ.push(
                    make_pair(v, distance[v]));

            predecessor[v] = u;
        }
    }
}
...

return distance[p_destination];}

【问题讨论】:

  • 你的实现是否正确?
  • 是的,我相信它是正确的,我在三个实现中得到了相同的输出(相同的寻路)。除了在第三个实现中运行时间超快。
  • 向我们展示你的实现
  • priority_queueset 之间的巨大差异可能是由更新顶点距离步长引起的。您可以使用set 简单地更新距离,但您不能使用priority_queue 来更新距离,例如,使用不同的距离多次添加相同的顶点。
  • 是的,有道理,这也是我的假设,因为 priority_queue 的巨大损失发生在队列中第一个元素的 pop 步骤中,它的复杂度为O(log(size_queue))

标签: c++ algorithm performance graph stl


【解决方案1】:

跳过

优先级队列让你的工作加倍工作真的很糟糕。

您是双重插入队列,因为您无法修改或删除。这是正常和必要的,因为你不能。

但是当这些旧值从队列中出来时,您需要“跳过 while 循环的迭代”。

类似:

if (PQ.top().second != distance[PQ.top().first]) continue;  // It's stale! SKIP!!

【讨论】:

  • 什么是陈旧节点?这是正确的吗:当我们处理节点 X 的邻居时,我们可能会发现到节点 Z 的距离小于记录的距离。所以我们更新记录的距离并将新的 {node Z, distance to Z} ** 推送到 PQ 上。然而,在以后的迭代中处理该节点之前,我们处理节点 Y 的邻居,发现到 Z 的距离小于刚刚记录的距离;因此,我们再次更新记录并将 {node Z, distance to Z} 推送到队列中。但是,之前添加的项目**仍然在PQ中。它已经过时了。
  • 因此,为了达到最优,我们应该只处理距 PQ 的距离 i 等于记录的距离数组中的距离(即 distance[i])的项目 {node i, distance i} .这个记录的距离数组包含迄今为止发现的最新(最短)距离。请让我知道我的理解是否正确。我相信这与此处 cmets 中讨论的内容相同:cs.stackexchange.com/a/118406/141696
  • 是的,完全正确。除了你不需要检查PQ.top().second &gt; dist[PQ.top().first] 并且只需!= 就可以了,而且通常会稍微快一些。这是因为从队列中出来的陈旧项目只能是&gt;,它们不能是&lt;,这取决于队列的排序方式以及新的、更短的路径被发现和插入的方式。是的,您链接上的讨论是关于同一件事的。
【解决方案2】:

std::priority_queue 的底层数据结构是一个最大堆,std::set 是它的自平衡二叉搜索——基本上是 C++ 的红黑树。所以两者都保证了O(logn)插入、删除和更新操作的时间复杂度。

但是,正如我提到的,std::set 的平衡二叉搜索树正在自动平衡,以保持其高度与节点数成对数,从而确保对数查询复杂性,而与插入顺序或任何操作之后无关。 std::priority_queue 不是自平衡的,根据插入顺序可能非常平坦。尽管自平衡有其自身的成本,因此在移除顶部后会变得越来越大,但我认为这就是性能提升的原因。

希望对你有帮助!

【讨论】:

  • 堆是一棵完全二叉树,它不能“非常平坦,取决于插入顺序”
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-30
  • 2021-04-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多