地图寻路搜索

项目中集成了高德开放平台地铁图 JS API 传送门,经过长期的功能迭代,甲方提出了各种定制化需求来优化体验,而使用这种第三方SDK就很难满足;因此如果自己开发一套地铁线路图引擎,从功能和体验上达到完全可控,可完美满足用户需求;

技术分析

  • 画布大小,元素坐标点确认,需要开发一套管理台,标记地图大小、元素坐标位置、元素连接关系等,在此不多赘述
  • 线路图绘制,这个实现方式有很多,移动WEB通过纯JS "svg"标签即可实现;如果采用RN技术栈,推荐使用react-native-svg第三方库实现;具体绘制实现思路轻点
  • 地图路径搜索,将整个地图的所有连接点保存成有向图,通过地图寻路算法快速找出最短路径,此次的重点就是如何实现寻路算法!
    image.png

重要概念

有向图

相对于无向图产生,无向图描述了所有顶点之间的连接关系,每两个顶点关系是双向的,而有相图不是,例如:A单向通向B,A->B,但反过来就不行;具体描述关系如下:
image.png
地铁线路图中所有的站点之间存在着连接关系,虽然说大部分情况前后站点连接是双向的关系,但不排除特殊情况下会出现单向连接,所以使用有向图来存储互相之间的关系比较稳妥。

广度优先(BFS)

广度优先遍历图的方式为,一次性访问当前顶点的所有未访问状态相邻顶点,并依次对每个相邻顶点执行同样处理,它是以一种类似波纹扩散的方式进行的,不断放大辐射半径,进而覆盖整张图。

深度优先(DFS)

深度优先遍历图的方式,同样会访问一个顶点的所有相邻顶点,不过深度优先的方式,首先访问一个相邻顶点,并继续访问该相邻顶点的一个相邻顶点,重复执行直到当前正在被访问的顶点出度为零,或者不存在未访问状态的相邻顶点,则回退到上一个顶点继续按照该深度优先方式访问。相对于广度优先访问,深度优先的方式更像是一条路走到黑,走不下去了再回到上个路口选择另外一条路。

Dijkstra算法

Dijkstra算法是按照广度优先(BFS)思想改进的搜索方案,引进两个集合S和U。S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离)。
初始时,S中只有起点a;U中是除a之外的顶点,并且U中顶点的路径是”起点到该顶点的路径”。然后,从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 然后,再从U中找出路径最短的顶点,并将其加入到S中;如发现路径更短的顶点,更新U中的顶点和顶点对应的路径。重复该操作,直到遍历完所有顶点。
Dijkstra动画示例

分析

按照广度优先的算法,像剥洋葱一样一层一层打开相邻顶点,直到找到目标顶点位置为止,虽然这种方案可以找到最短路近,但搜索较远距离的路径从性能上讲就比较耗时了,试想,很多反方向顶点都被打开参与搜索无疑比较费时。
同理,Dijkstra算法基于广度优先思路找到最短路近,但搜索较远距离的路径从性能上讲也比较耗时。
按照深度优先的算法,是一条路走到底,虽然说一旦找到目标顶点就马上停止,但不一定是最短路近,而快速搜索到目标顶点存在一定的运气成分,如果地图死胡同比较多的情况下也是比较费时。
有没有既不费时,又能找到相对较短路径的搜索算法?

A*算法

A*算法一种基于启发式探索的方法来提高Dijkstra算法的速度,在寻路的过程中,给每个节点绑定了一个估计值(即启发式),在对节点的遍历过程中是采取估计值优先原则,估计值更优的顶点会被优先遍历。所以启发函数的定义十分重要,显著影响算法效率。

启发函数

F(n) = G(n) + H(n)
G(n)表示由起点到顶点n的固定消耗,H(n)表示顶点n到终点的估计消耗,F(n)就表示由起点经过顶点n到达终点的总消耗;所以访问相邻顶点是以F(n)值较小优先遍历;
H(n)的计算方式有很多种,比如曼哈顿式H(n) = |dx|+|dy|,或者欧几里得式(两点距离)H(n) = sqrt(x^2 + y^2),本文采用欧几里得式;
按照启发函数,以路径损耗+预估损耗值优先的方式,当前顶点与目标顶点之间会越来越接近,达到减少探索范围,降低问题复杂度的目的;

实现流程

(1)从起点A开始,把它就加入到一个由顶点组成的 open list(开放列表) 中。open list 里的格子是路径可能会是沿途经过的,也有可能不经过。基本上open list是一个待检查的顶点列表。
(2)查看与起点A相邻顶点 (忽略障碍物或不可到达的顶点) ,加入到 open list中。把起点A设置为这些顶点的父亲。
(3)把A从open list中移除,加入到close list(封闭列表) 中,close list中的每个顶点都是现在不需要再关注的。
(4)检查所有与它相邻的顶点,忽略其中在close list中或是不可走的顶点,如果顶点不在open lsit中,则把它们加入到open list中。把我们选定的顶点设置为这些新加入的顶点的父亲。
(5)如果某个相邻的顶点已经在open list中,则检查这条路径是否更优,也就是说经由当前顶点到达那个顶点是否具有更小的 G 值。如果没有,不做任何操作。反之,如果 G 值更小,则把那个顶点的父亲设为当前顶点 ,然后重新计算那个顶点的 F 值和 G 值。
(6)从open list中选择 F 值最小的顶点,把它从open list里取出,放到close list中。
(7)重复步骤4~6,直到把终点加入到了open list中,此时路径已经找到了,或者查找终点失败,并且open list是空的,此时没有路径。
(8)从终点开始,每个顶点沿着父顶点移动直至起点,这就是搜索到的路径。

应用

地铁线路图作为有向图的数据结构,在确定起点和终点顶点位置后,通过A*的实现思路,从起点出发寻找周围的有向连接顶点,把它们加入到open list中,然后选择 F 值最小的顶点,把它从open list里取出,放到close list中,继续寻找该顶点周围的有向连接顶点,反复进行搜索,直到找到目标顶点位置为止。
image.png

总结

A*算法能够快速找到较短路径,在较大地图中使用,遍历获取F值最小的顶点优化以后速度还能提升数倍;在游戏地图、路径搜索类应用中使用该算法较为常见,如果出现山地、河流等制约条件,无非就是在启发函数中增加这些条件作为权值来改变优先级,从而快速得到最佳选择。