搜索总结

DFS 深度优先搜索

对于当前的一种点(通常表示一种情况)的所有分支,任选一条边走下去,直至递归回到当前点后,在考虑走其他分支。

我们通过递归实现的指数型、排列性和组合型枚举,就是 DFS 的三种最简单的形式。

因为其思维难度较低,DFS 深度优先搜索经常用来打暴力或者模拟。

其他使用 DFS 的都需要剪枝或者别样的搜索方式(具体见例题)。

迭代加深

深度优先搜索每次选定一个分支,不断深入,直至到达递归边界才回溯。这种策略带有一定的缺陷。试想以下情况:搜索树每个节点的分支数目非常多,并且问题的答案在某个较浅的节点上。如果深搜在一开始选错了分支,就很可能在不包含答案的深层子树上浪费很多时间。

我们可以采用迭代加深的方式来解决这样的情况,迭代加深是一种 每次限制搜索深度的 深度优先搜索。

迭代加深搜索的本质还是深度优先搜索,只不过在搜索的同时带上了一个深度 \(dep\) ,当 \(dep\) 到达设定的深度是直接返回。如果一次搜索没有找到合法的解,就让设定的深度加一,重新从根开始搜索。

既然是为了找最优解,为什么不用 BFS 呢?因为 BFS 本质上是通过队列来实现的,队列的空间复杂度很大,当状态比较多或者单个状态比较大时,BFS 就出现了明显的劣势。事实上,迭代加深就类似于用 DFS 实现的 BFS,而它的空间复杂度更小。

注意:在大多数的题目中,BFS 还是比较方便的,而且容易判重。当发现BFS 在空间上不够优秀,而且要找最优解的问题时,就应该考虑迭代加深

BFS 广度优先搜索

我们依靠一个队列来实现广度优先搜索。起初,队列中只有初始状态,在广度优先搜索的过程中,我们不断从队头取出状态,对于该状态的所有分支,把可以抵达的状态放进队尾。重复操作直至队列为空。

注:在以下描述中 \(s\) 是起点,\(dis\) 表示起点到当前点的最短距离

Bellman–Ford

Bellman–Ford 算法是一种基于松弛操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。

对于一条边 \((u,v)\) ,松弛操作如下: \(dis_v=min(dis_v,dis_u+w_(u,v))\)

这么做的是显然的:我们尝试用 \(S \rightarrow u \rightarrow v\) 去更新 \(v\) 点最短路的长度,若更优,就更新。

Bellman–Ford 算法所做的,就是不断尝试对图上每一条边进行松弛.我们每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法终止。

每次循环是 \(O(m)\) 的,那么最多会循环多少次呢?

在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少 \(+1\) ,而最短路的边数最多为 \(n-1\) 。所以最后时间复杂度是 \(O(nm)\)

SPFA

我们重新看到 Bellman-Ford ,很多时候我们并不需要那么多无用的松弛操作。

很显然,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。

那么我们用队列来维护可能会引起松弛操作的节点,就能只访问必要的边了。

虽然在大多数情况下 SPFA 跑得很快,但其最坏情况下的时间复杂度为 \(O(nm)\)

Dijkstra

所有边权为正,但可能存在重边和自环的图中,求一个点到另一个点的最短路的一种方法。

初始化: $dis_s=0 $ ,其余点的 \(dis\) 值均为 $ +\infty$

过程:

取出对头 \(x\) ,对于 \(x\) 的其中一个分支 \(y\) ,若当前节点的 \(dis\) 值加上边权小于 \(dis_y\) 则将 \(dis_y\) 更新并将 \(y\) 放入队尾(贪心)。重复操作直至队列为空。

在稀疏图中,时间复杂度是 \(O(n)\) ,但在稠密图中是 \(O(n^2)\) 的。

堆优化的 Dijkstra

可以用堆来优化这一过程:每成功松弛一条边 \((u,v)\) ,就将 \(v\) 插入堆中(如果 \(v\) 已经在堆中,直接减少他的值即可)。堆优化能做到的最优复杂度为 \(O(nlog_n+m)\)

特别地,可以使用优先队列维护,可以通过每次松弛时重新插入该结点,且弹出时检查该结点是否已被松弛过,若是则跳过,复杂度 \(O(mlog_n)\)

例题

P1462 通往奥格瑞玛的道路

思路很清晰 二分+最短路

首先,我们看到这里有两个信息,血和钱。

因为他只要最大钱最小,所以直接用二分答案来做。(每次看到极值就想用二分做)

我们可以这么想,如果给最短路中加一个花费上限 \(mid\),那么这个上限越大,那么跑的情况就越多,就越可能在没死前跑路,那么就产生了单调性:

mid越大,成功可能性越大

所以我们就二分 \(mid\),将二分出的 \(mid\) 带入\(dijkstra\)中,跑出扣的最少的血,如果小于 \(b\),说明符合条件,继续看是否有更小的,反之,向大取。

双端队列 BFS 与 优先队列 BFS

我们发现,上述问题中,每次移动的步数都是 \(1\) ,所以我们可以通过逐层搜索的方式解决起始状态到每一个状态最小步数的问题。

那如果每一此的步数不是 \(1\) 呢?换句话说,每次我们扩展状态时都有不同的代价,哪有该如何求解呢?

我们先考虑步数只有 \(0\)\(1\) 的情况

我们将问题化成 "在一张边权为 \(0\)\(1\) 的无向图上,求解起始点到每个点的最小路径。" 这种情况,我们考虑使用双端队列 BFS 来解决。整体的算法框架与普通 BFS 类似,只有在拓展时略作改变。当边权为 \(1\) 时,我们正常将他放置队末。当边权为 \(0\) 时,我们将新节点 从对头入队 。这样,我们仍然可以保证队列中的距离值的单调性。

如果步数不止有 \(0\)\(1\) 呢?

对于更普遍的情况,即每次扩展的代价不同时,求解起始点到每个点的最小路径。

方法一

仍然使用正常的广搜,仍然使用正常的队列。

这时我们不能再保证每个状态在第一次入队时就能得到最小代价,所以只能让一个状态多次进队,多次更新。不断搜索,直至队列为空。

这其实就是上文提到的 SPFA 算法。

方法二

使用优先队列进行广搜。

这里使用优先队列的作用就类似于一个二叉堆,每次取出的队顶一定是代价最少的状态。其余操作与普通 BFS 相同。

本质上其实与优先队列 Dijkstra 相同。

双向搜索

除了迭代加深以外,双向搜索也可以避免在深层子树上浪费时间。

在一些题目中,问题不但具有明确的初态,并且有明确的终态,并且从初态开始和从终态开始的搜索树都可以覆盖全部状态。所以双向搜索的基本思路就是从状态图上的起点和终点同时开始进行 DFS 或 BFS。产生两颗深度为一半的搜索树,在中间交汇,组合形成新的答案。

例题

P10484 送礼物

折半搜索。

看数据范围,\(2^{46}\) 肯定炸,但 \(2×2^{23}\) 炸不了。

整个数组从中间劈成两半,对于每一半,求出礼物能组成的所有重量数,存在两个数组中。

把其中一个数组排序,遍历另一个数组,二分查找在另一个数组中满足条件的最大值。最后求出所有答案的最大值即可。

A* 与 IDA*

其他剪枝

posted @ 2026-01-16 19:08  Austin0928  阅读(3)  评论(0)    收藏  举报