【问题标题】:Dijkstra's shortest path algorithm optimizationDijkstra的最短路径算法优化
【发布时间】:2021-04-18 16:43:02
【问题描述】:

我想首先说我的代码按预期工作,并且相当快。不管如何分析它,大部分时间都花在一个非常具体的部分上,这让我问:有没有普遍接受的更好的解决方案?

这是我的实现:

            var cellDistance = new double[cells.Count];
            cellDistance.SetAll(idx => idx == startCellIndex ? 0 : double.PositiveInfinity);

            var visitedCells = new HashSet<int>();

            do
            {
                // current cell is the smallest unvisited tentative distance cell 
                var currentCell = cells[cellDistance.Select((d, idx) => (d, idx)).OrderBy(x => x.d).First(x => !visitedCells.Contains(cells[x.idx].Index)).idx];

                foreach (var neighbourCell in currentCell.Neighbours)
                    if (!visitedCells.Contains(neighbourCell.Index))
                    {
                        var distanceThroughCurrentCell = cellDistance[currentCell.Index] + neighbourCell.Value;
                        if (cellDistance[neighbourCell.Index] > distanceThroughCurrentCell)
                        {
                            cellDistance[neighbourCell.Index] = distanceThroughCurrentCell;
                            prevCell[neighbourCell] = currentCell;
                        }
                    }

                visitedCells.Add(currentCell.Index);
            } while (visitedCells.Count != cells.Count && !visitedCells.Contains(endCell.Index));

大部分时间都花在了这一行上,它取的是未访问过的具有最低部分成本的节点:

var currentCell = cells[cellDistance.Select((d, idx) => (d, idx)).OrderBy(x => x.d).First(x => !visitedCells.Contains(cells[x.idx].Index)).idx];

更具体地说,在最后一个 lambda 中,不是那种(我觉得非常令人惊讶):

x => !visitedCells.Contains(cells[x.idx].Index)

由于visitedCells 已经是HashSet,仅使用内置数据结构我无法改进,所以我的问题是:是否有不同的方式来存储部分成本,使其具体化查询(即具有最低部分成本的未访问节点)明显更快?

我正在考虑某种排序字典,但我需要一个按值排序的字典,因为如果它按键排序,我必须将部分成本设为键,这使得更新它的成本很高,然后构成关于我如何将此结构映射到我的成本数组的问题,这仍然不能解决我的visitedCells 查找问题。

【问题讨论】:

    标签: data-structures dijkstra


    【解决方案1】:

    使用标志数组代替 HashSet

    HashSet 可以有 O(1) 的平均插入时间和预期查询时间。但是,由于您的节点 ID 只是数组中的索引,因此它们是连续的,并且不会增长太多。此外,您最终将拥有 HashSet 中的所有 id。在这种情况下,您拥有比使用“任何”通用哈希表更快的 O(1) 选项。您可以使用一个布尔数组来显示一个节点是否被访问,并使用节点 ID 对其进行索引。

    只需分配一个大小等于节点数的布尔数组。填写false。访问新节点时,将节点 id 的值设置为true

    遍历所有节点而不是对它们进行排序以选择下一个节点

    您当前的代码必须根据距离对所有节点进行排序,然后逐个遍历它们以找到第一个未访问的节点。由于排序,这在大多数情况下需要 θ(nlogn) 时间。 (可以进行优化以对节点进行部分排序,但如果编译器/库自己可以看到这个机会,那将是非常令人惊讶的。)使用这种方法,您的总时间复杂度变为 θ(n^2 * logn)。相反,您可以遍历节点一次,跟踪到目前为止看到的最小距离未访问节点。这适用于 θ(n)。总时间复杂度为 O(n^2),正如 Dijkstra 应该的那样。

    通过这两项更改,您的代码将没有多少 Dijkstra 最短路径不需要的内容。


    我正在考虑某种排序字典,但我需要 一个按值排序的,因为如果它按键排序,我必须 使部分成本成为关键,这使得更新它的成本很高,然后 提出了我如何将此结构映射到我的成本数组的问题

    有一种称为 min-heap 的数据结构,可用于从集合(连同其卫星数据)中提取最小值。一个简单的二元最小堆可以提取最小密钥或减少它在 θ(logn) 最坏情况时间内持有的一些密钥。

    在 Dijkstra 的情况下,您需要有一个稀疏图才能比迭代所有距离更有效(稀疏图 ≈ 边数远小于节点数的平方)。因为算法可能需要在每次松弛边缘时减少距离。

    如果有 θ(n^2) 条边,这使得最坏情况的总时间复杂度为 θ(n^2 * logn)。

    如果存在 θ(n^2 / logn) 边,则松弛时间变为 O(n^2)。然后,您需要一个比这个更稀疏的图,以使二叉堆比使用简单数组更有效。

    在最坏的情况下,从堆中提取所有最小距离节点需要 θ(nlogn) 时间,放松所有边需要 θ(e * logn) 时间,其中 e 是边数,总时间为 θ((n +e)登录)。正如我所说,只有当 e 渐近小于 n^2 / logn 时,这才比 θ(n^2) 更有效。

    【讨论】:

    • 感谢您的深入解释,它帮助了很多。将集合更改为数组对我的代码的影响为零,我将 prevCells 从字典更改为平面数组,影响也为零(尽管内存较少,因此总体上是一个加分项)。正如你提到的,它真正做到的是完全删除排序并循环成本一次,这将它从 640ms 带到了 40ms,所以一个数量级!
    • 在 40 毫秒时,我不会打扰最小堆,但我会记住它!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多