微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Dijkstra 的最短路径算法优化

如何解决Dijkstra 的最短路径算法优化

首先我想说我的代码按预期工作,而且速度相当快。无论对其进行分析,大部分时间都花在了一个非常具体的部分上,这让我不禁要问:是否有任何普遍接受的更好的解决方案?

这是我的实现:

            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)).OrderBy(x => x.d).First(x => !visitedCells.Contains(cells[x.idx].Index)).idx];

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

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

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

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

解决方法

使用标志数组代替 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) 更有效。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。