0x26 广搜变形

0x26 广搜变形

1.双端队列BFS

在最基本的广度优先搜索中,每次沿着分支的扩展都记为“一步”,我们通过逐层搜索,解决了求从起始状态到每个状态的最少步数的问题。这其实等价于在一张边权均为1的图上执行广度优先遍历,求出每个点相对于起点的最短距离(层次)。在第0x21节中我们曾讨论过这个问题,并得到了“队列中的状态的层数满足两段性和单调性”的结论。从而我们可以知道,每个状态在第一次被访问并入队时,计算出的步数即为所求。

然而,如果图上的边权不全为1呢?换句话说,如果每次扩展都有不同的“代价”,我们想求出起始状态到每个状态的最小代价,应该怎么办呢?我们不妨先来讨论图上的边权要么是1、要么是0的情况。

在这样的图上,我们可以通过双端队列广搜来计算。算法框架与一般的广搜类似,只是在每个节点上沿着分支拓展时稍作改变。如果这条分支是边权为0的边,就把沿该分支到达的新节点从队头入队;如果这条分支是边权为1的边,就像一半的广搜一样从队尾入队。这样一来,我们就仍然能保证,任意时刻广搜队列中的节点对应的距离值都具有“两段性”和“单调性”。

2.优先队列BFS

对于更加具有普适性的情况,也就是每次扩展都有各自不同的“代价”时,求出起始状态到每个状态的最小代价,就相当于在一张带权图求出从起点到每个节点的最短路。此时,我们有两个解决方案:

方法一

仍然使用一般的广搜,采用一般的队列。

这时我们不能保证每个状态第一次入队时就能得到最小代价,所以只能允许一个状态被多次更新、多次进出队列。我们不断执行搜索,直到队列为空。

整个广搜算法对搜索树进行了重复遍历与更新,直到“收敛”到最优解,其实也就是“迭代”的思想。最坏情况下,该算法的时间复杂度会从一般广搜的 O ( N ) O(N) O(N)增长到 O ( N 2 ) O(N^2) O(N2)。对应在最短路问题中,就是我们将在0x61节介绍的 S P F A SPFA SPFA算法。

方法二

改用优先队列进行广搜。

这里的“优先队列”就相当于一个二叉堆。我们每次可以从队列中取出当前代价最小的状态进行扩展(该状态一定是最优的,因为队列中其他状态的当前代价都不小于它,所以以后就更不可能在更新它了),沿着各条分支把到达的新状态加入优先队列。不断执行搜索,直到队列为空。

在优先队列BFS中,每个状态也会被多次更新、多次进出队列,一个状态也可能以不同的代价在队列中同时存在。不过当每个状态第一次从队列中被取出时,就得到了从起始状态到该状态的最小代价。之后若再被取出,则可以直接忽略,不进行拓展。所以,优先队列BFS中的每个状态只拓展一次,时间复杂度只多了维护二叉堆的代价。若一般广搜的复杂度为 O ( N ) O(N) O(N),则优先队列BFS的复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)。对应在最短路问题中,就是我们将在0x61节中介绍的堆优化 D i j k s t r a Dijkstra Dijkstra算法。

下图就展示了优先队列BFS的详细过程,每条边的数值标明了每个状态向下扩展所需的代价,节点中的数值标明了每个状态上求出的“当前代价”,灰色节点标明了每个时刻在堆中出现至少一次的节点。

0x26 广搜变形_第1张图片

0x26 广搜变形_第2张图片

建议不熟悉最短路相关算法的读者,简要对0x61节的两个单源最短路径算法进行阅读,这对于理解并写出正确的优先队列BFS有很大的帮助。

至此,我们就可以对BFS的形式,按照对应在图上的边权情况进行分类总结:

1.问题只计最少步数,等价于在边权都为1的图上求最短路。

使用普通的BFS,时间复杂度 O ( N ) O(N) O(N)

每个状态只访问(入队)一次,第一次入队时即为该状态的最少步数。

2.问题每次扩展的代价可能是0或1,等价于在边权只有0和1的图上求最短路。

使用双端队列BFS,时间复杂度 O ( N ) O(N) O(N)

每个状态被更新(入队)多次,只扩展一次,每一次出队时即为该状态的最小代价。

3.问题每次扩展的代价是任意数值,等价于一般的最短路问题。

(1)使用优先队列BFS,时间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)

每个状态被更新(入队)多次,只扩展一次,第一次出队时即为该状态的最小代价。

(2)使用迭代思想+普通的BFS,时间复杂度 O ( N 2 ) O(N^2) O(N2)

每个状态被更新(入队)、扩展(出队)多次,最终完成搜索后,记录数组中保存了最小代价。

3.双向BFS

0x24节中我们介绍了双向搜索的思想,双向BFS与双向DFS的思想完全相同。因为BFS本身就是逐层搜索的算法,所以双向BFS的实现更加自然、简便。以普通的求最少步数的双向BFS为例,我们只需从起始状态、目标状态分别开始,两边轮流进行,每次各扩展一整层。当两边各自有一个状态在记录数组中发生重复时,就说明这两个搜索相遇了,可以合并得出起点到终点的最少步数。

你可能感兴趣的:(#,0x20,搜索,算法,c++)