.NET 6 在已知拓扑路径的情况下使用 Dijkstra,A*算法搜索最短路径

背景介绍

bc33e177d1d6cf831a4350f215de84ef.png

突然闯到路径搜索算法里来,缘起是需要在一个项目中实现拓扑路径中的最短路径搜索,应用领域是工业机器人。

在计算机科学中,寻找图中两个节点之间的最短路径是一个重要的问题。Dijkstra算法是一种广泛应用的最短路径算法之一,能够有效地找到图中节点之间的最短路径。在已知图的拓扑结构的情况下,Dijkstra算法是一种高效的解决方案。

A算法是一种基于启发式搜索的路径搜索算法,通常用于图或网络中的最短路径问题。它结合了Dijkstra算法的完备性和贪心搜索的高效性,在启发函数的指导下沿着图搜索最短路径。A算法采用估计函数(启发式函数)来估算当前节点到目标节点的成本,并在搜索过程中优先考虑估计成本最小的节点。

本文将探讨这些算法在已知拓扑路径的情况下如何搜索最短路径。

1. 什么是Dijkstra算法?

Dijkstra算法是一种单源最短路径算法,用于计算图中一个节点到其他所有节点的最短路径。该算法的核心思想是从起始节点开始,逐步扩展搜索范围,通过不断更新节点的最短距离来找到目标节点的最短路径。Dijkstra算法是基于贪心策略的,它保证已经找到的最短路径是当前已知最短路径中最短的。
030ca9a6653c7953c72ca88f8caecfc8.png

2. 已知拓扑路径的情况下的Dijkstra算法

当已知图的拓扑结构时,Dijkstra算法可以更快速地找到最短路径。在这种情况下,我们可以利用已知的路径信息来优化算法。一般情况下,我们通过一个邻接矩阵或邻接表来表示图的拓扑结构。邻接矩阵可以明确地表示节点之间的连接关系,而邻接表则更为灵活,可以表示节点之间的连接并存储相关权重信息。

在已知拓扑路径的情况下,我们可以利用这些信息进行优化。Dijkstra算法通常使用一个优先队列(或最小堆)来管理候选路径,但在已知路径的情况下,我们可以直接使用已知的路径信息,而不需要通过优先队列来动态地搜索和更新路径。

例如,如果我们需要找到从节点A到节点B的最短路径,而且我们知道A到B的直接路径,那么我们可以直接使用这条路径,而不必在整个图上运行Dijkstra算法。这样可以极大地提高搜索的效率,尤其是在大型图中搜索最短路径时。

为了在代码中实现路径搜索,我们利用A算法来实现这一点,但其实经过退化后的A, 实际的算法仅保留Dijkstra算法。

这里介绍的A*算法,主要通过下面这个函数来计算每个节点的优先级。

5a8c0a17a3d352e18b44974c81abd27c.png

其中:

  • f(n)是节点n的综合优先级。当我们选择下一个要遍历的节点时,我们总会选取综合优先级最高(值最小)的节点。

  • g(n) 是节点n距离起点的代价。

  • h(n)是节点n距离终点的预计代价,这也就是A*算法的启发函数。关于启发函数我们在下面详细讲解。

A算法在运算过程中,每次从优先队列中选取f(n)值最小(优先级最高)的节点作为下一个待遍历的节点。
另外,A
算法使用两个集合来表示待遍历的节点,与已经遍历过的节点,这通常称之为open_set和close_set。

3. 应用场景

f15a4bd1eca2f33377eac0dead9e93f7.png

  • 大致是这样的场景,地图内已经构造好可行驶的路径,比如,从A点,可以按照 A->B->D 或 A->C->D 或A->E->D 三条路径到达D点,那么怎么计算出一条最短的路径呢?

  • 又或者如果A->E不能行驶,怎么再次进行路径规划,计算出最短路径呢?

4. 代码的准备工作

先定义节点和边,构造一个边和点的集合拓扑。在节点类中,算法关键的G和H都在这里定义,当然也少不了Parent,在最终构造路径时,需要反溯回去。

public class PathNode: IEquatable<PathNode>
    {
        public string Id { get; set; }
        public float X { get; set; }
        public float Y { get; set; }
        public bool IsBreak { get; set; }
        /// <summary>
        /// 真实路径成本
        /// </summary>
        public double G { get; set; } 
        /// <summary>
        /// 启发式路径成本
        /// </summary>
        public double H { get; set; }
        /// <summary>
        /// 总成本
        /// </summary>
        public double F => G + H; 

        public PathNode Parent { get; set; }

        public PathNode(string id)
        {
            this.Id = id;
        }

        public static bool operator ==(PathNode lhs, PathNode rhs)
        {
            return string.Compare(lhs?.Id, rhs?.Id) == 0;
        }
        public static bool operator !=(PathNode lhs, PathNode rhs)=> !(lhs == rhs);
        public override int GetHashCode()
        {
            return Id?.GetHashCode() ?? 0;
        }
        public override bool Equals(object obj)
        {
            if (obj == null) return false;
            return string.Compare(Id, (obj as PathNode)?.Id) == 0;
        }
        public override string ToString()
        {
            var b = IsBreak ? "禁行" : String.Empty;
            return $"{Id}[b]";
        }

        public bool Equals(PathNode other)
        {
            if (other == null) return false;
            return string.Compare(Id, other?.Id) == 0;
        }

        // 重建路径
        public List<PathNode> ReconstructPath()
        {
            List<PathNode> path = new List<PathNode>();
            var currentNode = this;
            while (currentNode != null)
            {
                path.Add(currentNode);
                currentNode = currentNode.Parent;
            }
            path.Reverse();
            return path;
        }
    }

边长的定义,依赖于点,定义如下:

public class PathEdge: IEquatable<PathEdge>
   {
       public PathEdge(PathNode start,PathNode end, float distance)
       {
           Direction = true;
           Distance = distance;
           IsBreak = false;
           this.StartNode = start;
           this.EndNode = end;
       }
       /// <summary>
       /// 起始点
       /// </summary>
       public PathNode StartNode { get; set; }
       /// <summary>
       /// 结束点
       /// </summary>
       public PathNode EndNode { get; set; } 

       /// <summary>
       /// 距离,单位 m
       /// </summary>
       public float Distance { get; set; }

       /// <summary>
       /// 行走方向, true,正走,否则倒走
       /// </summary>
       public bool Direction { get; set; }
       /// <summary>
       /// 断开
       /// </summary>

       public bool IsBreak { get; set; }

       public string Id => $"{StartNode?.Id}->{EndNode?.Id}";

       public override string ToString()
       {
           var dir = Direction ? "正走" : "倒走";
           var b = IsBreak ? "禁行" : String.Empty;
           return $"{Id}[{b} 长度:{Distance},{dir}]";
       }

       public static bool operator ==(PathEdge lhs, PathEdge rhs)
       {
           return lhs?.Id == rhs?.Id;
       }
       public static bool operator !=(PathEdge lhs, PathEdge rhs) => !(lhs == rhs);
       public override int GetHashCode()
       {
           return $"{Id}".GetHashCode();
       }
       public override bool Equals(object obj)
       {
           if (obj == null) return false;
           return this == (obj as PathEdge);
       }

       public bool Equals(PathEdge other)
       {
           return this == other;
       }
   }

拓扑集合,我们实现了IReadOnlyCollection接口,以便可以枚举节点。

public class MapPaths : IReadOnlyCollection<PathNode>
    {
        private readonly Dictionary<string, PathNode> _pathNodes = new Dictionary<string, PathNode>();
        private readonly Dictionary<string, PathEdge> _pathEdges = new Dictionary<string, PathEdge>();
        /// <summary>
        /// 事务ID
        /// </summary>
        public string Id { get; set; }

        public void InitData(List<PathNode> nodes, List<PathEdge> edges)
        {
            
            _pathNodes.Clear();
            _pathEdges.Clear();
            if (nodes != null)
            {
                foreach (var item in nodes)
                {
                    _pathNodes.Add(item.Id, item);
                }
            }
            if (edges != null)
            {
                foreach (var item in edges)
                {
                    _pathEdges.Add(item.Id, item);
                }
            }
            
        }

        /// <summary>
        /// 从点集合内 增加边,
        /// </summary>
        /// <param name="start"></param>
        /// <param name="end"></param>
        /// <param name="distance"></param>
        /// <returns></returns>
        public PathEdge AddEdges(string start, string end, float distance)
        {
            
            if (!_pathNodes.ContainsKey(start) || !_pathNodes.ContainsKey(end))
            {
                return null;
            }
            var startNode = _pathNodes[start];
            var endNode = _pathNodes[end];

            var tempEdge = new PathEdge(startNode, endNode, distance);
            _pathEdges[tempEdge.Id] = tempEdge;
            return _pathEdges[tempEdge.Id];
            
        }

        public void Reset()
        {
            
            foreach (var item in _pathNodes)
            {
                item.Value.IsBreak = false;
            }
            foreach (var item in _pathEdges)
            {
                item.Value.IsBreak = false;
            }
            
        }


        public PathNode GetNodeByID(string id)
        {
            PathNode node;
            if (_pathNodes.TryGetValue(id, out node))
            {
                return node;
            }
            else
            {
                return null;
            }           
        }
        /// <summary>
        /// 获取指定点的邻居点集合
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        public List<PathNode> GetNeighbors(PathNode node)
        {
            return _pathEdges.Values.Where(x => !x.IsBreak  &&   x.Id.StartsWith(node.Id)).Select(x=>x.EndNode).ToList();
        }

        /// <summary>
        /// 两点之间的距离
        /// </summary>
        /// <param name="currentNode"></param>
        /// <param name="neighbor"></param>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
        public double DistanceBetween(PathNode currentNode, PathNode neighbor)
        {
            var tempEdge = new PathEdge(currentNode,neighbor, 0 );
            if(this._pathEdges.ContainsKey(tempEdge.Id))
            {
                var find = this._pathEdges[tempEdge.Id];
                return find.Distance;
            }
            else
            {
                return 0D;
            }
            
        }
        public int Count => _pathNodes.Count;

        public IEnumerator<PathNode> GetEnumerator()
        {
            return _pathNodes.Values.GetEnumerator();
        } 

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _pathNodes.Values.GetEnumerator();
        }

        
    }

5. A*算法的实现

利用Parallel进行并行化计算,提升计算枚举的效率。
这里实现了典型的A*算法, 然而由于启发函数H,按照0进行计算,因此失去了启发的意义,本质上已经退化为Dijkstra算法

public static List<PathNode> AStar(PathNode startNode, PathNode endNode, MapPaths grid)
        {
            if(startNode == null || endNode == null || grid == null)
            {
                return new List<PathNode>();
            }
            var openSet = new ConcurrentDictionary<PathNode, bool>();
            var closedSet = new ConcurrentDictionary<PathNode, bool>();

            openSet.TryAdd(startNode, true);

            while (openSet.Count > 0)
            {
                var currentNode = GetLowestFScoreNode(openSet.Keys);

                if (currentNode == endNode)
                {
                    return currentNode.ReconstructPath();
                }

                openSet.TryRemove(currentNode, out _);
                closedSet.TryAdd(currentNode, true);

                var neighbors = grid.GetNeighbors(currentNode);

                Parallel.ForEach(neighbors, neighbor =>
                {
                    if (neighbor.IsBreak || closedSet.ContainsKey(neighbor))
                        return;

                    double tentativeGScore = currentNode.G + grid.DistanceBetween(currentNode, neighbor);

                    if (!openSet.ContainsKey(neighbor) || tentativeGScore < neighbor.G)
                    {
                        neighbor.Parent = currentNode;
                        neighbor.G = tentativeGScore;
                        neighbor.H = grid.DistanceBetween(neighbor, endNode);

                        openSet.AddOrUpdate(neighbor, true, (key, oldValue) => true);
                    }
                });
            }

            return null;
        }
        
        

        // 获取具有最低 F 值的节点
        private static PathNode GetLowestFScoreNode(ICollection<PathNode> openSet)
        {
            PathNode lowestNode = null;
            double lowestFScore = double.PositiveInfinity;

            foreach (PathNode node in openSet)
            {
                if (node.F < lowestFScore)
                {
                    lowestFScore = node.F;
                    lowestNode = node;
                }
            }

            return lowestNode;
        }

6. 一个应用例子

我们构造一堆路径,其中设定了路径的长度,然后计算从 N02到N03的路径。

MapPaths paths = new MapPaths();

            List<PathNode> nodes = new List<PathNode>()
            {
                new PathNode("N01"),
                new PathNode("N02"),new PathNode("N03"),
                new PathNode("N04"),new PathNode("N05"),
                new PathNode("N06"),new PathNode("N07"),
                new PathNode("N08"),new PathNode("N09"),
            };           

            paths.InitData(nodes,null);
            paths.AddEdges("N02", "N03", 1).IsBreak = true;
            paths.AddEdges("N02", "N04", 8);
            paths.AddEdges("N04", "N03", 1);
            paths.AddEdges("N05", "N03", 1);
            paths.AddEdges("N02", "N05", 2).IsBreak = true;
            paths.AddEdges("N02", "N06", 1);
            paths.AddEdges("N06", "N07", 1);
            paths.AddEdges("N07", "N05", 1).IsBreak = true;
            paths.AddEdges("N07", "N06", 1);
            paths.AddEdges("N07", "N08", 1);
            paths.AddEdges("N08", "N05", 1);
            //paths.GetNodeByID("N07").IsBreak = true;
            var path1 = PathTools.AStar(nodes[1], nodes[2], paths);
            var s1 = string.Join("->", path1.Select(x => x.Id));

返回的结果如下:

N02->N06->N07->N08->N05->N03

7. 优势与应用

Dijkstra算法在已知拓扑路径的情况下具有明显的优势。通过利用已知路径信息,我们可以避免对整个图进行完整的搜索,从而节省计算资源和时间。这种优化对于需要频繁搜索最短路径的应用场景尤为重要,比如路由算法、网络传输优化以及GPS导航系统等。

总结

Dijkstra算法和A*算法是一个强大且广泛应用的最短路径算法,在已知拓扑路径的情况下尤为高效。通过利用已知的路径信息,我们可以大大提高搜索最短路径的效率,避免对整个图进行不必要的遍历。在实际应用中,我们可以根据具体场景利用算法的这一优势,提高系统的性能和响应速度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值