【算法】树搜索
2022-11-14 13:49:03

0.为什么要树搜索?

有许多问题,它们的解可以表示为树。举个例子:

布尔表达式可满足性问题

q输入:由$x_1,x_2,\cdots,x_n$n个逻辑变元组成的逻辑表达式$A$。

输出:指派$v$,令$A^v=T$,即一种变元赋值,让表达式为真。

对于每一个变元,都有0,1或者F,T两种取值,那么我们可以构造出这样一棵树来:

于是乎,我们可以应用树的相关算法,求解这样的一些问题。

8-puzzle问题

该问题根据每一次挪动,可以被转化为树搜索问题:

首先我们先介绍两个算法:DFS,BFS。之后我们讨论如何优化树搜索的相关算法。

1.深度优先搜索(DFS)

对于一个图,我们想知道两个点之间是否有一条通路,可以使用DFS算法。

DFS算法有种“不撞南墙不回头”的感觉,算法如下:

  1. 将起点压栈,标记为访问过。(从起点)
  2. 将一个与栈顶点邻接的,且未被访问过的节点压栈,标记为访问过,递归此步。(一直走下去)
  3. 如果最后没有没访问过的节点,且目标点没有遍历过,则回溯,将当前节点弹出,重新选择节点压栈。(直到走不通,往回走)

2.广度优先搜索(BFS)

BFS相比于DFS,就灵活很多,它有种“扩散”的感觉,算法如下:

  1. 将起点入队,标记为访问过。(从起点)
  2. 将与起点所有邻接点入队,标记为访问过,同时将起点出队,递归此步。(扩散)

3.爬山法

对于一个可以用树搜索+DFS解决的问题,在深度优先搜索中,如何判定令哪个节点入栈最优呢?

我们可以使用贪心算法,即每一步都选令局部最优的那个解。

对于8-puzzle问题中的每一个步骤,我们可以编写出一个代价判据,每次都根据栈顶点,选择让这个代价判据最小的新点入栈。

通俗的来讲,就是在DFS“一直走下去”的过程中,每次都选择最“方便”的那一个点。

举个例子,我们让代价判据为“所有处于错误位置的方块数”,那么应用爬山法:

就可以找到一个解啦。

4.Best-First搜索算法

但是,这不够好,贪心是局部最优的,有的时候局部最优显然不是全局最优解,Best-First算法就是为了解决这个情况而生的。

我们同样设立一个代价判据,但是,在所有节点中选择令代价判据最小的方案。

同样对于8-puzzle问题,如果应用这个算法:

  1. 先走第一层,出现3,3,4,4的代价。
  2. 让两个3代价的展开,出现3,4,2,4的代价。
  3. 展开2代价的点。
  4. $\cdots$

5.分支限界法

除此之外,我们可以采用这样的模式:把不可行的解都排除,只留下可行的解,再在可行解空间内讨论最优解。

6.旅行商问题(TSP)

思路

给定一个有向图,这个图满足如下条件:

  1. 每个节点都没有到自己的边。
  2. 每对节点间都有一条非负加权边。

输出从任意一个节点开始,经过每个节点一次,最终返回开始节点的最短路径。

对于这个问题,我们采用如下的步骤求解:

  1. 将所有解集合作为树根,设定一个基础代价,由代价矩阵计算所有解的代价的下界。
  2. 用爬山法递归地划分解空间,得到二叉树。

划分过程:

  1. 选择图上的任意一条边$(i,j)$划分,该划分要让左子树代价下界不变,右子树代价下界增加最大;且所有不包含$(i,j)$的解集合作为左子树,不包含的解集合作为右子树。
  2. 计算左右子树代价下界。

划分到最后会出现一个解,这个解的代价$\alpha$是所有解的代价上界,如果对于其他子节点来说,它们的下界超过了$\alpha$,就停止继续划分。

这样的求解方法相当于:先将所有解汇总起来,通过二叉树的搜索方法,排除掉那些代价高的,最终筛选出一个低代价的解。

基础代价的设定

代价矩阵里,包括着两点间每一条边的长度,如果不存在边,就设为$\infty$,举个例子:
$$
\left[
\begin{matrix}
\infty & 3 & 5 & 9\
4 & \infty & 6 & 8\
7 & 3 & \infty & 2\
3 & 5 & 7 & \infty\
\end{matrix}
\right]
$$
第$i$行,第$j$列的点表示$(i,j)$的长度。

基础代价肯定是几条边的长度的和,怎么选呢?我们不妨选择一些最小边,让每一行、每一列都有被选择出来的边。后续根据这些边再构造解。

我们在每一行都减去一个最小值:
$$
\left[
\begin{matrix}
\infty & 0 & 5 & 9\
0 & \infty & 6 & 8\
7 & 3 & \infty & 0\
0 & 5 & 7 & \infty\
\end{matrix}
\right]
$$
同时把所有减去的值加起来。这是为什么?

此外,要求:每一列都有0,每一行都有0。因为我们最终要把每个点都跑一遍。于是矩阵变成:
$$
\left[
\begin{matrix}
\infty & 0 & 0 & 9\
0 & \infty & 6 & 8\
7 & 3 & \infty & 0\
0 & 5 & 7 & \infty\
\end{matrix}
\right]
$$
初始代价为$3+4+2+3+5=17$。这就是我们的代价下界。

划分

选择让右子树代价下界增加最多的划分边$(i,j)$,这又怎么选呢?

左子树代价不能变,所以只能从矩阵中那些为0的边中选择。其次,右子树是不包括划分边的,我们可以在矩阵中把划分边所在的行、列都不予考虑,重新构造代价下界,让剩下的每一行、每一列都出0。

之后在左右子树内都不断再分,可以选择先分割左子树,最终得到一个代价上界。

7.A*算法

一个很有效的搜索最短路径的算法。

原理:
$$
f(n)=g(n)+h(n)
$$
其中,对于任一节点$n$,$f(n)$是经过节点$n$到目标节点的代价,$g(n)$是从起始节点到节点$n$的代价,$h(n)$是从节点$n$到目标节点的最小估计代价。

我们可以采用Best-First算法和BFS算法结合实现对$h(n)$的估计,从起始节点开始跑BFS,每一个节点的$h(n)$就是这个节点所有未被遍历过的边的最小值。