树搜索算法
树搜索策略的引入
三个基本问题:
- 什么样的问题能转化为树?
- 转化为树有什么好处?
- 转化后的树节点/边有什么含义,代表了什么?
以下面的布尔表达式、8-Puzzle问题、寻找哈密顿环为例,解答这三个问题
布尔表达式可满足性问题
输入:n个布尔变量x1,x2,···,xn;关于这n个变量的k个析取布尔式
输出:找到一种使这k个布尔析取式均为真的一种x1~xn的赋值
对于这个问题,最开始想到的就是穷举了,将所有x1~xn的赋值情况都列出来,然后选择那些满足k个析取式为真的赋值组合。问题是如何穷举,怎么穷举有次序,达到不重不漏
首先想到的是按照x1~xn顺序去赋值:首先给x1进行赋值(T/F),然后继续给x2进行赋值(T/F)····如果到了xi,发现不能使这k个析取式均为真,则重新对xi进行赋值。最后得到一组赋值组合,使这k个析取式为真
我们可以将这个问题转化为树,利用树的路径是唯一的,保证某个赋值组合只被穷举过一次;而且可以通过从叶子节点回溯到root根节点,回溯得到这个赋值组合;如果当前对xk的赋值不能满足使析取式为真,则可以回溯到它的父亲节点,进行另一种赋值
将树的边作为对xi的赋值情况,树中每一个节点可以看做是经过从根节点到该节点的赋值过程后这k个析取式的状态(为真/为假)
最终可以从叶子节点回溯到root节点,记录各边的赋值情况,即为该问题的一个可行解
8-Puzzle问题
输入:具有8个编号小方块的魔方
输出:移动系列,经过这些移动,魔方可以达到如上图所示的状态
同样思考穷举,每次移动一个编号,找最终可以达到上面这种状态的移动系列
将问题转化为树,每次只移动一个编号,树的节点代表了当前魔方的状态,由于每一步的状态都依赖于上一步的状态,这种关系可以用树中的父子关系来表示——将父亲节点代表的魔方状态移动某一个编号,即为该父亲节点的儿子节点。最终的解可以从叶子节点回溯到根节点得到
哈密顿环问题
输入: 具有n个节点的连通图G=(V, E)
输出: G中是否具有Hamiltonian环
这是NP-hard问题,可以使用穷举进行求解。思路:从一个节点出发,然后到达与该点邻接的节点,若遍历所有节点并最终回到起点,则找到了一条哈密顿环,若路径的中间重复出现了某个非起点的节点,或者不能回到起点,则说明该路径不能获得一条哈密顿环
将问题转化为树:将当前遍历到的节点用树的节点表示,若最终遍历到root节点,则从该叶子结点回溯到root节点,可以得到这条哈密顿环
一个规模更小的例子:
总结
由这三个问题的例子,可以得到树搜索问题的基本特征:
- 什么样的问题能转化为树?
- 需要穷举得到解
- 每一步的状态都依赖于上一步的状态,可以用树中的父子节点表示这种关系
- 问题的解是一个系列,需要回溯得到,树中从叶子到root节点的回溯可以得到这样的解
- 转化为树有什么好处?
- 从树的root节点到某个叶子节点的路径是唯一的,这保证了穷举的唯一性,即某一个解系列不会被重复列举,做到了不重
- 树的节点可以表示状态,树的边可以表示操作,通过树的搜索过程,能将所有的情况都穷举出来,做到了不漏
- 若某一步操作/状态不能满足约束,可以在树中回溯节点(经过的节点会储存在栈或队列中),回到某个仍然满足约束的节点,然后选择另一条路径,继续进行搜索
- 树中的一条路径就代表一个穷举过程。最终的解是树中一条从叶子节点回溯到root节点的路径
- 转化后的树有什么含义(怎么设计出这个树)?
- 树的节点可以表示状态,树的边可以表示操作
- 首先要确定出穷举策略,然后用树的节点/边对应穷举过程中的状态/操作
- 用适当的树搜索策略对节点进行展开,获得问题的可行解
下面介绍两种启发式搜索策略,它们能有目的地展开特定的节点,从而尽可能提高效率,获得可行解。注意:只是可行解,不是最优解
基本的树搜索策略
就是BFS与DFS
BFS搜索策略
算法过程与原来的BFS没什么区别,就是要增加一个判断:看当前队列Q中的第一个元素x是否为目标节点。如果是,则结束BFS,获得一个可行解,否则就继续执行BFS
DFS搜索策略
从S中挑出一个数字集合,求和等于9
树的节点代表当前和的状态(Sum)
树的边代表加上某个数字的操作(+num)
与BFS搜索策略一样,如果DFS维护的栈中的栈顶元素Top(S)为目标节点时,就停止DFS搜索,否则继续进行DFS
这两种策略都有盲目性,穷举最大的特点就是问题的规模会很大,很难解决,如果盲目地搜索,时间复杂度会非常高
优化的树搜索策略
第三个分支限界搜索是专门针对组合优化问题,它是爬山法和best-first策略的进一步拓展,所以它的基本策略还是爬山法或best-first策略
爬山法
基本思想
爬山法是贪心法+DFS,每次在叶子结点中,选择当前最接近正确解的节点,然后对它进行扩展
为了描述这种与正确解的接近程度——引入测度函数f(n)
在8-Puzzle问题中,用节点n中,处于错误位置的方块数作为测度。测度越小,则越接近于正确解
最后要达到的状态:
这里2、8、1处于错误的位置,则f(n)=3
爬山法解决8-Puzzle问题:
在前一步展开的节点的儿子中(DFS,向深处理),选择测度函数值最小的节点(贪心),然后继续对该子节点进行展开。重复上述步骤,直至得到正确解,或者栈S为空,算法结束
伪代码
对比DFS搜索策略与爬山法的步骤,发现二者只有第4步是不同的
DFS将S所有的孩子无特定顺序地压入栈中,而爬山法将S的子节点按照其启发测度由大到小的顺序压入S中,每次压入之后要检查栈顶元素是否为目标节点。可以看到爬山法较DFS,对节点的展开有更加的针对性,每次都优先展开更加趋于正确解的节点。
然而爬山法的缺点也是明显的,这种贪心策略是局部的,并不能使整体效率最优:爬山法每次只对树的一支进行贪心、展开,而这一支不能保证有正确解,或者正确解需要大量的穷举才能得到,可能会导致这一支都穷举结束后,再返回到root节点,展开另一支,效率不高
Best-First搜索策略
基本思想
Best-First搜索策略是 广度优先思想+贪心
借鉴广度优先的思路,Best-First策略将当前树中所有可能展开的点都纳入贪心范围内,找其中最接近正确解的节点。这样就不会陷入树的一支,而是每次选出全局的最优情况,这样的贪心能提高整体搜索的效率。
仔细回想一下我们在最短路径算法时也用到了类似的搜索策略,从这个节点向外扩张,不断更新所有节点到达这个节点的最小代价,不断更新这个代价,重复这个过程,每次都选最短的路径进行扩张,期待每次最后能得到到该顶点的最短路径.
伪代码
使用堆作为数据结构,测度最大 or 最小的节点在堆顶处,每次展开这个节点即可
实例:8-Puzzle问题
组合优化问题的搜索策略——分支限界
上述两种搜索策略最终只能得到一个可行解,而组合优化问题要求出最优解,所以上述两种策略不适用于组合优化问题的树搜索
基本思想
本质仍然是穷举,去找最优的那个解。但是在找最优解过程中,我们可以通过当前最接近于最优解的可行解去确定某一支穷举的策略一定得不到最优解,从而排除一部分节点,即不再展开这些节点,进而缩小解空间,提高效率。而获得可行解可以使用上面的两种策略,所以分支限界中的搜索策略仍然是爬山法 or Best-First搜索策略
可以先用爬山法尽快的获得一个可行解,然后再用Best-First搜索策略再去搜索,寻找最优解。
多阶段图搜索问题(最短路径)
输入:多阶段图
输出:从v0到v3的最短路径
树的节点是图中遍历到的节点,树边的权值是两个节点之间的距离
爬山法进行分支限界搜素
由于要获得最优解,所以要将整棵树都进行搜索,直至没有节点可以展开(最终叶子节点要么是可行解,要么是被可行解排除掉的)
注意:获得的可行解都是最优解的上界
自己模拟一遍这个过程,体会一下使用可行解排除掉部分节点的过程
分支限界策略的原理
说白了就是用爬山法或者Best-First方法找到一个可行解,再利用这个可行解对其他扩展进行剪枝(分支限界法的关键)
人员安排问题
问题定义
任务是有偏序关系的,人有编号,人与工作是一一对应的关系,而且要求具有偏序关系的任务Ji<Jk
,分配的人编号必须满足Pi<Pk
本质上就是给一些具有偏序关系的任务进行编号,编号从小到大,要求具有偏序关系的任务之间,先进行的任务编号<后进行的任务编号(编号小的先进行,大的后进行)
这自然联想到拓扑排序问题,毕竟AOE网就是这个东西。获得所有拓扑序列,然后从小到大编号即可
转换为树搜索问题
用树表示所有拓扑排序序列,树的层数就相当于编号(根节点在第0层),这样就能将拓扑排序序列对应进行编号了
有点AOE网的感觉,父节点<子节点(表示偏序关系,不是小于)或者 父节点与子节点没有偏序关系,为了构成搜索树,要加一个根节点0,它的子节点是初始偏序集中所有没有前序元素的元素
拓扑序列树的生成算法
输入:偏序集合S,树根root
输出:由S的所有拓扑排序序列构成的树
生成树根root
选择偏序集中所有没有前序元素的元素,作为root的子节点
for root的每个子节点v do
S=S-{v};
把v作为根,递归地处理S.
实例
Pij——第i个人分配第j项任务的代价
练习:使用爬山法和Best-First的分支限界搜索算法做一遍,感觉还是先进行一次爬山法,获得一个可行解,之后再使用best-first进行分支限界更好一些
![image-20231223002032092](https://i-blog.csdnimg.cn/blog_migrate/77b9b2a68431714e814d357802964857.png)
![image-20231223002047806](https://i-blog.csdnimg.cn/blog_migrate/7793bac427321b06d4e28f5f94e12440.png)
![image-20231223002115443](https://i-blog.csdnimg.cn/blog_migrate/bb8c0229a72158a3b87514b7d828bca2.png)
节点的值是对应的Pij的值,在进行分支限界时,要判断当前的指派方式的代价之和是否已经超过可行解的代价。剪枝——减掉的是当前获得的局部解一定差于最优解的策略
在分支限界搜索过程,由于分支之间的差距不大,导致往往遍历到一支中最后几个节点处才能剪枝,剪枝的效果很差。所以简单地使用分支限界的方法是不行的,需要做出优化
分支限界优化——解的下界
每一种指派都有代价的下界(每一行 或 一列中最小的代价),则可以得到一个解的下界,相应的,代价矩阵要减去这个下界,则Pij为将第j个任务指派给第i个人,解的下界的增值。例如选择P12,则包含P12的解的下界要在初始下界54的基础上增加4,当前解的下界为58
注意:解代价下界不必是可行解,如本例,解的下界中包含了P23与P43,并不满足问题要求
构造解的下界的技巧:代价矩阵每行或每列减去同一个数(该行或列的最小数),使每行和每列都至少有一个零,其余各元素非负
新的加权树表示
根节点0的值为这个下界,其它节点的值是 父节点的值+Pij,是包含当前指派的解的下界
练习:用爬山法再重新做一遍,体会引入解的下界如何提高剪枝的效率(增大了各个节点值之间的差距,方便剪枝,分支限界最好用于各个节点的值差距很大的情况)
最终结果
旅行商问题
问题定义
输入:无向连通图G,G的特征:每个节点都没有到自己的边,没对节点之间都有一条非负加权边
输出:一条哈密顿回路,特征:一条由任意一个节点开始,经过每个节点一次,最后返回开始节点的路径,且该路径的代价最小(组合优化问题)
转换为树搜素问题
实例
本例是在一个有向完全图中寻找哈密顿回路,是一般的哈密顿回路问题的简化
问题的划分
首先将代价矩阵转换为代价下界的增值矩阵——减去每行/每列中最小的值,使矩阵的各行各列都至少有一个0
结果如下:
Cij:从i直接到j,代价下界增加的值
如从4直达6,C46=0,则在当前的代价下界96上加C46=0,就是包含从4直达6的解的代价下界,为96
f(i,j):从i到j经历其它节点(不直达),最小的代价下界增值
如f(4,6)
,排除掉(4,6),第i
行是从第i
个节点到各个顶点的代价下界增值,第j
列是从各个顶点到第j
个节点的代价下界增值,所以在第4行寻找除了C46外最小的代价下界增值,为C41=32;然后再从第6列寻找除了C46外最小的代价下界增值,为C56=0,所以从4经过其它顶点到达6的最小代价下界增值为C41+C56=32
而路径最终是从4->1->···->5->6,由于1->5之间的路径代价增值是非负的,所以4->6(不直达)的代价增值一定不小于32,所以32是4->6(不直达)的最小代价下界增值
为了尽早地进行剪枝,我们要选择使子节点代价下界增加最大的划分边(4,6),作为树的划分
这里(4,6)是各个Cij=0
的边(i,j)
中,最小代价下界增值f(i,j)
最大的
所以用解中是否包括(4,6)作为问题的划分
计算左右子节点的代价下界
递归构造左右子树
构造左子树根对应的代价下界增值矩阵
左子树是已经包含了(4,6)的解的集合,为了避免出现回路,左子树不能再出现4->···->6,且不能出现(6,4),所以要将所有4可达的C4j与所有可达6的C6i均删除掉(其实应该是对这些节点做标记,后续不再处理它们了,相当于删除掉了),然后对(6,4)赋值为无穷,避免后续选择它
继续递归处理左子树,与前面相同,先检查是否有某一行或一列不为0的情况,若有,则同时减掉这一行或一列最小的值,然后在原来的代价下界上增加这个减去的值,即为左子树根的代价下界。上面的矩阵是第5行已经减掉3的结果了
构造右子树根对应的代价下界增值矩阵
初始矩阵:
右子树是不包含边(4,6)的解的集合,所以只需要把C46赋值为无穷(可以有4->i->j->k->···->6,所以第4行和第6列不需要删除)
再检查各行各列是否至少有一个0,与前面操作相同,若有,则同时减掉这一行或一列最小的值,然后在原来的代价下界上增加这个减去的值,即为右子树根的代价下界。
当前树的状态
爬山法扩展左子树
0-1背包问题
问题定义
转换为树搜索问题
下界——贪心算法的可行解
上界——分数背包问题的最优解
实例
A*算法(重要)
A*算法详解(个人认为最详细,最通俗易懂的一个版本)-CSDN博客
A*最终得到的解一定是优化解,不需要将节点全部展开
A*算法常用Best-First算法搜素
f*(n)=g(n)+h*(n),f*(n)是节点n的代价,g(n)是从root到n的代价,h*(n)是从n到目标节点的优化路径的代价
h*(n)需要估计,而且要估计出的h*(n)要比真实的h*(n)代价小,即h(n)≤h*(n)恒成立
实例
选择与节点v相连的边中最小的权值,作为h*(v)的估计h(v)
A*算法的规则
当根据Best-First策略,要选择目标节点进行扩展时,A*算法结束,可以回溯出一个有化解
正确性证明
- 根据Best-First策略可知,当进行最后一次展开时,要选择节点
t
,说明有f(t)≤f(n)
,又因为f(n)≤f*(n)
,则有f(t)≤f*(n)
- 假设有
{f*(n)}
有一个为最优解f*(s)
,则有f(t)≤f*(s)
- 又因为
f(t)=g(t)
是一个可行解,而f*(s)
是有化解的代价,则有f(t)=g(t)≥f*(s)
成立,两边夹,最终有f(t)=g(t)=f*(s)
为有化解
反证法
也就是ni不在路径里面,在进行Best-First过程中,要选择当前代价F最小的节点进行展开,由于f(ni)<f(TP),则应该优先展开节点ni,而不是TP,所以矛盾
例题
参考:
哈工大算法设计与分析之树搜索问题_哈工大 算法设计与分析-CSDN博客
树的搜索问题2——分支界限和A*算法(多阶段图问题、人员安排问题和旅行商问题)_a star算法属不属于分支界限算法-CSDN博客