全双路最短路径问题的最快实现?

时间:2011-08-23 17:51:13

标签: c algorithm implementation dijkstra shortest-path

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

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

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

提前多多感谢!!

欧金尼奥

5 个答案:

答案 0 :(得分:12)

我查看了您在评论中发布的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,整个循环将有9亿次迭代。这很痛苦(正如你所见),但是没有理由做这么多工作。

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

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修复为更优化的实现,但我认为这里的正确选择只是抛弃此代码并找到更好的Dijkstra算法实现。例如,如果您使用the pseudocode from the Wikipedia article实现binary heap,则可以在O(| E | log | V |)中运行Dijkstra算法。在您的情况下,这个值刚刚超过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实例应该没问题。

希望这有帮助!

答案 1 :(得分:3)

Floyd-Warshall算法怎么样?

答案 2 :(得分:2)

您的图表是否有任何特殊结构?图表是平面的(或几乎是这样的)?

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

申请是什么?你确定当你需要找到一条特定的最短路径时,做双向Dijkstra(或A *,如果你有一个启发式)会不够快吗?

答案 3 :(得分:0)

如果你可以将算法修改为多线程,你可以在不到24小时内完成它。

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

答案 4 :(得分:0)

瓶颈可以是您使用存储路径的数据结构。如果使用太多存储空间,很快就会耗尽缓存和内存空间,导致快速算法运行速度非常慢,因为它会获得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);
        }
    }
}