引子
搜索,就是通过一定算法对状态空间进行分割,选出所需的解。
1. 搜索算法的形象化描述
抽象为图
形象化搜索问题有助于我们设计相关的算法。举个例子,对于如下的二阶汉诺塔问题:
设有三根钢针,它们的编号分别是1号、2号和3号。在初始情况下,1号钢针上穿有A、B两个金片,A比B小,A位于B的上面。要求把这两个金片全部移到另一根钢针上,而且规定每次只能移动一个金片,任何时刻都不能使大的位于小的上面。
我们可以做出如下的抽象:
设定一些状态,用二元组表示,对于任意时刻$k$,任意的二元组$S_k=(S_{k0}, S_{k1})$,$S_{k0},S_{k1}$分别表示A和B所在的钢针号。
接着,我们可以抽象出这样一张图:
从$(1,1)$到$(2,2)$或$(3,3)$的所有路径都是可行解。
抽象为树
这种抽象方法很简单,程序也易于编写,思想是:从起点开始,将所有的可能节点列为子节点。
但是,这种方法会导致抽象出来的结果出现大量重复的结构。
与或树
一个问题,可以被分解为不同的部分,解决所有部分也就解决了该问题;可以被分割成不同的等价的子问题,解决任一子问题,也就相当于解决了该问题。当这两种分割方式同时存在时,就可以使用与或树表示。
与或树里包含着几种节点:
- 叶子节点
- 与节点:与节点代表的问题被分割成很多部分。
- 或节点:或节点代表的问题被分割成很多等价的子问题。
看个例子吧:
叶节点用弧表示的,是与节点;不带弧的,是或节点。根节点引出的“2”“3”两个节点就是与节点。
2. 深度优先搜索
若能够将问题抽象为图,便可以应用图论的相关算法,深度优先搜索(DFS)便是其中一个。它可以用来搜索一条从起点到终点的路径。
若想进行深度优先搜索,则需要用到栈。
该算法的思想是,逐条探寻可行路径。
该算法步骤如下:
- 从起点开始,沿着任意一条边探索下去。将探索到的点入栈,并将其标记为探索过的点。
- 若探索到一个顶点,该顶点还有未被探索过的后继点,则将该后继点入栈,做标记。若该顶点的所有后继点都被探寻过了,则将该点出栈。
- 若探索到最后还有没探索到的点,则表明该图非连通图,则选择任意一个未被探索过的点当作起点,回到步骤1。
拿一个问题来说:
问题摘录如下:
按照字典序输出自然数 1 到 n 所有不重复的排列,即 n 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。
C语言代码如下:
1 |
|
本题中,将问题抽象为森林,对于三层的情况,两棵树如下:
在dfs()
函数中,先判断是否为应该输出答案的状态,若非,则一层一层的探寻可行的状态。
3. 广度优先搜索
广度优先搜索(BFS)需要用到队列。
该算法思想是:从起点开始不断扩散,寻找可行解。
该算法步骤为:从起点开始,将所有未探索的后继点加入队列,将起点出队。重复此步骤。
3.5 双向搜索
您也可以从起点、终点同时开始深搜、广搜,能一定程度上降低复杂度。
4. 代价一致搜索
在广度优先搜索的步骤中,我们假定每次拓展都是等代价的,这样我们可以同时拓展未探索的节点。然而在实际生活中,对于不同的节点,扩展的代价往往不同。那么我们先设定拓展节点的代价,接着每次拓展时,都拓展总代价最小的那一个节点。代价可以通过如下的公式计算:
我们假设$g(n_1)$代表从初始节点到$n_1$节点的路径代价,$c(n_1,n_2)$是从$n_1$到$n_2$的代价,那么有:
$$
g(n_2)=g(n_1)+c(n_1,n_2)
$$
举个例子,对于如下的图:
假设A为起点,E为终点,搜索步骤如下:
- 先探索A。
- 探索C,因为A到C的代价为1,A到B代价为3。
- 探索B,因为若探索D,代价为4,探索B代价为3。
- 探索D。
- 探索E,探索到了终点,程序终止。
该方法探索到的路径一定是代价最小的路径。
5. 贪婪最佳优先搜索
上面所提到的搜索方法都是无信息的搜索方式,即没有任何信息,直接开始搜索。
从该搜索方式开始,都属于有信息的搜索方式。
和代价一致搜索略有区别,该搜索方法的代价为到目标节点的剩余距离。每次扩展,只扩展代价最小的子节点。
同样对于上面这张图,探索步骤如下:
- 先探索A。
- 探索C。
- 探索D。
- 探索E。
该方法没法保证最终能够得到路径,假设能得到路径,也没法保证最佳性。
6. A*算法
代价一致搜索考虑的是已经走过的距离,贪婪最佳优先搜索算法考虑的是余下的距离,而A*算法则将这两种算法结合起来。
该算法中,节点$n_1$代价函数为:
$$
f(n_1) = g(n_1)+h(n_1)
$$
其中$f(n_1)$是节点$n_1$的代价函数,$g(n_1)$是起始节点到节点$n_1$走过的距离,$h(n_1)$是节点$n_1$到目标节点的估计代价。
如您所见,估计代价函数是很重要的,设定估计代价函数时,一定要保证估计代价要小于等于实际的代价。过高地估计,有可能会让算法错误地放弃真实的最短路径。
7. 博弈搜索
博弈搜索主要讨论在确定的、全局可观察的、竞争对手轮流行动的对抗搜索两人对决游戏。
极小极大分析法
对于博弈问题,可以生成一个与或树。与节点、或节点交替层级出现。奇数层为己方先手,偶数层为对方先手。此外,我们还有一个代价函数,假定为对你的有利程度。或节点表示己方可选的下法,与节点则表明我需要考虑对方所有可能的落子方法。
利用极小极大分析法,我们需要从叶子节点向上推导,对“或”节点, 选其子节点中一个最大的得分作为父节点的得分;对“与”节点, 选其子节点中一个最小的得分作为父节点的得分。奇数层节点(我方节点)总是会选择赢面最大的子节点状态,而偶数层(对方节点)总是会选择我方赢面最小的的子节点状态。
Alpha-Beta剪枝
极大极小分析法要分析的节点太多了,如果能有一种算法剪去一些枝,则能一定程度上降低复杂度。
偷个懒,引用一下别人的文章,您能在参考资料中找到原文:
(1) 对于一个与节点MIN, 若能估计出其倒推值的上确界β,并且这个β值不大于MIN的父节点(一定是或节点)的估计倒推值的下确界α,即α≥β, 则就不必再扩展该MIN节点的其余子节点了(因为这些节点的估值对MIN父节点的倒推值已无任何影响了)。这一过程称为α剪枝。
(2) 对于一个或节点MAX, 若能估计出其倒推值的下确界α, 并且这个α值不小于MAX的父节点(一定是与节点)的估计倒推值的上确界β, 即α≥β,则就不必再扩展该MAX节点的其余子节点了(因为这些节点的估值对MAX父节点的倒推值已无任何影响了)。 这一过程称为β剪枝。
所谓MIN节点,就是与节点。
蒙特卡洛树搜索
我们也可以用类似抽样的方法进行搜索。
该方法由四个步骤组成:
- 选择
- 拓展
- 模拟
- 反向传播
最开始,我们将需要决策的局面设置为根节点。对于蒙特卡洛搜索树中的每一个节点,都包含着三个信息:
- 该节点代表的局面。
- 该节点被访问的次数。
- 累积评分。
同时,节点可以被分成三类:
- 被完全评估过的节点。拿国际象棋举例,也就是对于该节点来说,所有的可能走法都被评估过了。
- 未被完全评估的节点。拿国际象棋举例,也就是对于该节点来说,还有一些落子方法没有被评估过。
- 结束节点。拿国际象棋举例,也就是对于该节点来说,一方将另一方将死。
在选择阶段,我们从根节点开始,逐步评估子节点,对于上面的三种节点,分别采取不同的策略:
- 若节点被完全评估,则计算所有子节点的评分,则将评分最高的子节点设为待评估节点,进行选择操作。
- 若节点未被完全评估,则该节点设为待评估节点,进行拓展操作。
- 若节点为结束节点,则该节点设为待评估节点,进行反向传播操作。
在拓展阶段,我们在待评估节点的基础上,随机选择一个动作A,在搜索树中创建一个子节点,该子节点表示待评估节点经过动作A所转换得到的局面,接着进行模拟操作。
在模拟阶段,我们在上一步得到的子节点的基础上,随机进行游戏,直到得到一个结束节点,若结束节点代表己方被将死,则视为失败,反之视为成功。接着进行反向传播操作。
在反向传播阶段,从结束节点开始,回溯更新父节点的评分值。
当算法结束时,访问次数越高的节点往往是最佳的选择。
计算评分,常常使用应用到搜索树上的上限置信区间算法UCT,公式如下:
$$
UCT=\frac{w_i}{n_i}+C\sqrt{\frac{lnN_i}{n_i}}
$$
其中$w_i$表示选择$i$节点的胜利次数,$n_i$是$i$节点的模拟次数,$C$是一个平衡系数,$N_i$是全部的模拟次数。该公式的形式,来自正态分布的置信区间的计算公式,您可以在参考资料中查看相关的资料。