Skip to the content.

Graphs and Graphs Searching Algorithms

Graph的一些基本知识

这些知识以前有学过,如图中的vertexedgepathneighbor/adjacent等。介绍一些没见过的:

Graph’s variants

weighted graphs:每条边上都有一个给定的权重,大多数图不允许负数权重。比如说航线图中,边上的权重代表里程数。

directed graph:有向图,边是有方向的。

SPL:basic graph

SPL中有一个BasicGraph类,代表了一个带权重的有向图,是基于类Graph实现的。

image-20240229132526047

Graph Searching Algorithm

目标是寻找两个顶点之间的路径,探索特点是:在回溯之前,对于每一条路径尽可能深入的探索。

image-20240301100959394

BFS(Breadth-first searching)

目标是探索两个顶点之间的路径,搜索特点是,优先探索每个节点的所有邻居,探索完一个节点的所有邻居后马上回溯,之后探索该节点的父节点的下一个邻居节点(与上面的节点属于同一深度),当前深度探索完毕后,深度+1,不停地执行,直到找到目标路径或探索结束。

每次只专注于探索一个路径。

注意,在探索时,和当前节点属于同一批次邻居的节点不能被探索,如下图中,g,h,f是一个批次的,虽然存在g->h的路径,但是此路径是不允许被探索的。

image-20240301103007540

算法实现思想(bfs from v1 to v2)

上面说过我们会维护一个queue来保存将要访问的节点,刚开始只有一个初始节点v1,我们会重复如下过程,直到队列为空(队列是先进先出):

同时探索多个可能的路径。

image-20240301135555997

此外,还可以根据需要求去定制化算法,如找到目标步数的路径,找到所有可能的路径等等。

DFS,BFS runtime(成本分析)

时间复杂度O(V+E):两种算法的时间复杂度和空间复杂度与图的顶点数量V和边数E有关系,在最坏的情况下,DFS和BFS算法都要把每个顶点/每条边都要检查一次,且只会检查一次。因此时间复杂度应该是O(V+E)

后面会有课程专门分析为什么时间复杂度是O(V+E),先留个疑问

空间复杂度未做说明其实无论是时间复杂度还是空间复杂度,都和采用的数据结构有关,如采用邻接表和采用邻接矩阵的运行成本可能是不同的。


下面讲的Dijkstra算法和A*算法都是在BFS算法的基础上进行进一步改进的版本,目的是提高BFS的执行速度。

适用于图的规模十分庞大的场景。此外,这些算法考虑了权重因素,最短路径往往不一定是权重和最低的路径,我们想要的也不总是最短路径,也可能是成本最低的路径。

这里讲解的算法没有考虑任何优化,是最原生的版本,适用于所有具有不同特点的图。

Dijkstra’s Searching-BFS算法的改良版

在一个带权重的有向图中,给定一个起始vertex,dijkstra算法可以找到从该给定点到其他任意顶点的最小权重和路径。算法基本思想如下:

比如说,寻找最优路径问题,城市是vertexs,路程长度是带权重的边,Dijkstra算法可以找到从一个给定city到其他任何一个城市的最短里程路径。当每条边的权重都相同时,Dijkstra算法就退化为BFS搜索算法。

该算法具有多种不同的变种,可以在某些类型的图中最大化发挥作用。可以自行了解这些变种。

注意:该算法不适用于具有负权重边的图。

伪代码思想

我们使用dijkstra算法寻找从v1到v2的最短权重路径,需要做的工作如下所示:

初始化:创建一个表记录v1到其他每个顶点的成本大小,初始值设为正无穷,这意味着我们还不知道从v1到达这些顶点的任何路径。当然v1到自己的成本肯定是0;创建一个priority quque,将所有vertexes放进去,按照成本从小到大依次放入(这就是判断优先级的准则),刚开始队列中只有v1。

由于放入节点时采用的是从小权重到大权重得到思想,因此我们总是能先找到低成本的路径,且找到目标节点的路径之后,就可以停止了。探索思想与BFS方法是十分相似的。

当prioriy queue(下称pqueue)不为空时:

当完成上述过程后,我们已经可以通过previous节点指针重构出从v2回到v1的最小权重路径。这种算法不会遍历所有路径,一般来说只会探索pqueue之中在目标点之前的所有小权重路径。

image-20240302141827040

不足之处

假设在一个二维迷宫里,我们想要找到到某个地点的最优路径,假设采用了Dijkstra算法,算法会默认向所有方向进行探索,但是我们知道终点是在某一个方向上的,因此很多路径的探索都是无用功,原生的Dijkstra算法不能充分的利用图中现有的信息,他只是根据步骤,不停地探索自己所有方向上的邻居。

我们是否可以给算法一些hints,用一个更聪明的方式去探索,尽量避免做无用功?

A* Searching-在Dijkstra算法基础上加上启发式规则

Heuristics(启发式规则)

用一个推测、估算或者有根据的猜测,来引导算法找到一个解决方案。唯一准则是离目标越近越好

admissible heuristic:这种启发式方法估算出来的距离值是小于或等于实际答案的。这是一种保守、乐观的估算,称之为可接受的启发式规则。这种方法往往不会考虑图中会不会存在障碍物等现实因素。

我们要考虑如何在Dijkstra算法的基础上制定一个合理的admissible heuristic,引导算法少做无用功。

启发式规则的设定直接影响了算法的效果,如果我们设定的启发式规则比实际最短路径要长,那么这会使得算法找不到最短路径,即在找到实际的最短路径之前算法就会停止搜索。

A*算法思想

A*算法是Dijkstra算法的改进版本:加入一个启发式规则函数来引导路径的探索顺序。这种规则必须能够引导算法更快的、正确的找到最短的路径。在大多数情景下,A*算法会具有比Dijkstra算法表现得更好,会探索更少的顶点而找到最短路径,

假设我们要寻找从顶点a到顶点c的路径,b节点可以看做是任意一条路径中的中间节点:

因此,A*算法实际是在DijkStra算法的基础上改变了pqueue中计算优先级的方式,使之更适合现实情况:

A*算法的伪代码思路如下: image-20240303104648328

根据伪代码来看,A*算法与Dijkstra算法有如下的几个不同之处,我们使用A*算法寻找从v1到v2的最小成本路径,下面将Dijkstra Cost统一称为D(v1,v2)Heuristic Cost统一称为H(v1,v2)

注意,pqueue中存储的是每个节点的final cost,而成本表数据结构中依然只存储每个节点的Dijkstra成本。