【问题标题】:Fastest implementation for All-pairs shortest paths problem?所有对最短路径问题的最快实现?
【发布时间】:2011-08-23 17:51:12
【问题描述】:

我有一个加权图 30k 节点 160k 边,没有负权重。 我想计算从所有节点到其他节点的所有最短路径。 我认为我不能假设任何特定的启发式方法来简化问题。

我尝试使用这个 Dijkstra C 实现http://compprog.wordpress.com/2007/12/01/one-source-shortest-path-dijkstras-algorithm/,这是针对单个最短路径问题,为我所有的 30 个节点调用函数 dijkstras()。可以想象,这需要很长时间。目前我没有时间自己编写和调试代码,我必须尽快计算这些路径并将它们存储在数据库中,所以我正在寻找另一种可以使用的更快的解决方案,你有吗有什么建议吗?

我必须在最近的 8GB 内存的 macbook pro 上运行它,我想找到一个不超过 24 小时完成计算的解决方案。

提前非常感谢!!

尤金尼奥

【问题讨论】:

  • 那你有什么建议?我尝试了这段代码compprog.wordpress.com/2007/12/01/…,计算一个节点的所有最短路径需要一分钟多的时间。而且这需要做 30k 次所以不可行。
  • 只是为了确定您是在打开优化标志而不是在调试模式下编译此代码吗?
  • 这不是调试模式,但我没有使用任何选项。标志,我不确定缺点
  • @LastCoder 这不是调试模式,但我没有使用任何选项。标志,我不确定缺点,你建议什么优化?
  • 请注意,您不必从所有节点到所有其他节点执行 Dijkstra - 一旦您知道从 A 到 B 的最短路径,您也知道从 B 回到 A 的最短路径。您还知道从该路径上的每个节点到该路径上的每个其他节点的最短路径。

标签: c algorithm implementation dijkstra shortest-path


【解决方案1】:

我查看了您在 cmets 中发布的 Dijkstra 算法链接,我相信这是您效率低下的根源。在内部 Dijkstra 循环中,它使用一种极其未优化的方法来确定接下来要探索的节点(在每个步骤中对每个节点进行线性扫描)。有问题的代码在两个地方。首先是这段代码,它试图找到下一个要操作的节点:

mini = -1;
for (i = 1; i <= n; ++i)
    if (!visited[i] && ((mini == -1) || (d[i] < d[mini])))
         mini = i;

由于此代码嵌套在访问每个节点的循环中,因此复杂度(如链接中所述)为 O(|V|2),其中 |V|是节点数。在你的情况下,因为 |V|是 30,000,这个循环总共会有九亿次迭代。这非常缓慢(如您所见),但没有理由必须做这么多工作。

这里还有一个麻烦点,它试图找出图中应该使用哪条边来降低其他节点的成本:

for (i = 1; i <= n; ++i)
   if (dist[mini][i])
       if (d[mini] + dist[mini][i] < d[i])
           d[i] = d[mini] + dist[mini][i];

这会扫描邻接矩阵中的一整行以寻找要考虑的节点,这需要时间 O(n),无论有多少出边离开该节点。

虽然您可以尝试将这个版本的 Dijkstra's 修复为更优化的实现,但我认为正确的选择是丢弃此代码并找到 Dijkstra 算法的更好实现。例如,如果你使用 the pseudocode from the Wikipedia articlebinary heap 实现,你可以得到 Dijkstra 算法在 O(|E| log |V|) 中运行。在您的情况下,这个值刚刚超过 200 万,比您当前的方法快大约 450 倍。这是一个巨大的差异,我敢打赌,通过更好的 Dijkstra 实现,您最终会在比以前更短的时间内完成代码。

除此之外,您可能需要考虑并行运行所有 Dijkstra 搜索,正如 Jacob Eggers 指出的那样。该凸轮可为您拥有的每个处理器提供额外的速度提升。结合上述(更重要的)修复,这可能会给您带来巨大的性能提升。

如果您计划在更密集的数据集(边数接近 |V|2 / log |V| 的数据集)上运行此算法,那么您可能需要考虑切换到Floyd-Warshall 算法。每个节点运行一次 Dijkstra 算法(有时称为 Johnson's algorithm)需要 O(|V||E| log |V|) 时间,而使用 Floyd-Warshall 需要 O(|V|3)时间。但是,对于您提到的数据集,图表足够稀疏,运行多个 Dijkstra 的实例应该没问题。

希望这会有所帮助!

【讨论】:

  • 这是一个全面的答案! A+。
  • @templatetypedef 感谢您的精彩分析!我尝试使用此代码cs.rochester.edu/~nelson/courses/csc_173/graphs/apsp.html 切换到弗洛伊德的算法,这显然是 O(|V|3),但它仍然非常非常慢......一个节点大约需要 3-4 分钟(这个时间对你有意义吗? ),计算所有路径意味着 3mins*30k。正如我所写的,我没有太多时间,这就是为什么我试图避免从头开始重写和调试实现。我真的很惊讶我没有在 Web 上找到任何高质量的 Dijkstra 用 C 语言实现!
  • 这完全是一个无耻的自插件,但不久前我用斐波那契堆写了一个 Dijkstra 算法的实现(以 O(E + V log V) 运行,非常快)。代码在我的个人网站上在线(Java):keithschwarz.com/interesting/code/?dir=dijkstra
  • @Eugenio- 正如我在回答中提到的,我强烈建议不要在这里使用 Floyd-Warshall,因为它比重复调用 Dijkstra 的算法效率低得多。如果你时间紧迫,你最好花几个小时寻找/编写一个好的 Dijkstra 算法并使用它,因为你投入的时间减少执行时间将使获得结果成为可能,而不是使用一个缓慢的版本,不会给你任何东西。
  • @templatetypedef 再次感谢;无论如何,我尝试优化 cs.rochester.edu/~nelson/courses/csc_173/graphs/apsp.html 实际上代码不需要 3 个矩阵 30kx30k 而只需要两个(你可以只使用矩阵 D 和 P,对吗?) .在此更改和 -O gcc 标志设置之后,现在每个节点的代码不到 2 秒,这意味着我可以在半天之内运行所有代码。另外,实际上,在我的图表中,a->b 的权重始终与 b->a 相同,我可以利用它还是应该考虑到这一点?
【解决方案2】:

Floyd-Warshall 算法怎么样?

【讨论】:

  • 由于边与节点的比率较低,Floyd-Warshall 的执行速度往往比 Dijkstra 算法的重复迭代慢。 FW 的复杂度为 O(V^3),而多个 Dijkstra 调用需要 O(VE + V^2 log V)。如果 E = O(V),这将减少到 O(V^2 log V)。由于 OP 的输入相当大,我希望多个 Dijkstra 调用运行得更快。
【解决方案3】:

你的图表有什么特殊的结构吗?图形是平面的(或几乎是平面的)?

我建议不要尝试存储所有最短路径,一个非常密集的编码(30k^2“下一步去哪里”条目)将占用 7 gigs 的内存。

什么是应用程序?当您需要找到特定的最短路径时,您确定执行双向 Dijkstra(或 A*,如果您有启发式算法)不够快吗?

【讨论】:

  • 我不认为它是平面的或具有其他特殊结构的,为什么要说 7gigs?这是 900M 条目对吗?该应用程序是基于 Web 的,并且需要立即得到答案,因此我想将路径存储在数据库中,您是否认为要求特定路径可以在不到两秒的时间内完成? @rrenaud
  • 我认为每个条目会花费 8 个字节。但实际上,如果你有 30k,log(30k) ~= 15,所以你可以逃脱每个条目 2 个字节。
【解决方案4】:

如果您可以将算法修改为多线程,您或许可以在 24 小时内完成。

第一个节点可能需要超过 1 分钟。但是,第 15,000 个节点应该只需要一半的时间,因为您会计算到所有先前节点的最短路径。

【讨论】:

    【解决方案5】:

    瓶颈可能是您用来存储路径的数据结构。如果您使用过多的存储空间,您很快就会耗尽缓存和内存空间,从而导致快速算法运行非常缓慢,因为它获得了 100(缓存未命中)或 10000+(交换页)常数乘数的顺序。

    因为您必须在数据库中存储路径,我怀疑这可能很容易成为瓶颈。最好尝试首先使用非常有效的存储模式生成进入内存的路径,例如每个顶点 N 位,其中 N == 每个顶点的最大边数。然后为可用于生成最短路径之一的每个边设置一个位。生成此路径信息后,您可以运行递归算法,将路径信息生成为适合数据库存储的格式。

    当然,最可能的瓶颈仍然是数据库。您需要仔细考虑使用什么格式来存储信息,因为在 SQL 数据库中插入、搜索和修改大型数据集非常慢。如果数据库引擎设法将多个插入操作引导到单个磁盘写入操作,那么使用事务执行数据库操作也可能能够减少磁盘写入开销。

    将结果简单地存储在内存缓存中并在不再需要解决方案时丢弃解决方案会更好。如果您碰巧再次需要它们,则再次生成相同的结果。这意味着您将仅在实际需要时按需生成路径。对于 Dijkstra 的单个所有最短路径运行,30k 节点和 160k 边的运行时间应明显低于一秒。

    对于最短路径算法,我一直选择 C++。没有任何理由说明 C 实现也不简单,但是 C++ 提供了可用于初始实现的 STL 容器的简化编码,并且只有在基准测试和分析表明需要某些东西时才稍后实现优化的队列算法比 STL 提供的更好。

    #include <queue>
    #include "vertex.h"
    
    class vertex;
    class edge;
    
    class searchnode {
        vertex *dst;
        unsigned long dist;
        public:
        searchnode(vertex *destination, unsigned long distance) :
            dst(dst),
            dist(distance)
        {
        }
    
        bool operator<(const searchnode &b) const {
            /* std::priority_queue stores largest value at top */
            return dist > b.dist;
        }
    
        vertex *dst() const { return dst; }
    
        unsigned long travelDistance() const { return dist; }
    };
    
    
    static void dijkstra(vertex *src, vertex *dst)
    {
        std::priority_queue<searchnode> queue;
    
        searchnode start(src, 0);
        queue.push(start);
    
        while (!queue.empty()) {
            searchnode cur = queue.top();
            queue.pop();
    
            if (cur.travelDistance() >= cur.dst()->distance())
                continue;
    
            cur.dst()->setDistance(cur.travelDistance());
            edge *eiter;
            for (eiter = cur.dst()->begin(); eiter != cur.dst()->end(); eiter++) {
                unsigned nextDist = cur.dist() + eiter->cost();
    
                if (nextDist >= eiter->otherVertex())
                    continue;
    
                either->otherVertex()->setDistance(nextdist + 1);
    
                searchnode toadd(eiter->othervertex(), nextDist);
                queue.push(toadd);
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-03-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-07
      • 1970-01-01
      相关资源
      最近更新 更多