搜索技巧
概述
NOIP的搜索主要考察代码能力,思维难度不高。常用搜索的方法有DFS,BFS和迭代加深。常见优化有剪枝、状态压缩、双向搜索和启发式搜索
基本概念
DFS
适合状态存储不了的情况,相对符合人类的思考习惯(我们只需要考虑当前的子问题)
代码难度相对BFS较为简单
BFS
适合状态容易存储的问题(难以处理序列操作等,需要压进结构体),适合搜索深度可能会很深,甚至不知道有多深的情况
ID迭代加深
确定深度上限并进行DFS,可以方便回溯且防止一条路走到黑。常用来解决确定步数的题目,可以把最优化问题转化为可行性问题。
优化
剪枝
剪枝可以极大地降低搜索的效率,同时也使其复杂度难以衡量(常骗分)。
剪枝方法的特殊性比较强,必须因题而异,一般地说,剪枝可以分为两类:
- 可行性剪枝:某个状态一定到达不了目标状态
- 最优化剪枝:某个状态的所有到达目标状态的方案一定不是最优值
出现这两种情况就说明这个状态及后续状态都不用再访问
例1:P1731 [NOI1999]生日蛋糕
首先考虑可行性
一个比较简单的,当前体积>规定体积则退出
思考剪枝的一个重要思路是在极端情况下能不能满足题目要求
那么对于某个状态,如果剩下的层数全部让体积最大,都不能达到规定的体积,那么这个状态就可以cut掉
其次考虑可行性剪枝
研究题目性质可以发现,俯视图的面积在最开始的时候就确定了
所以我们每层搜索只需要考虑侧边的高度
这里同样可以考虑极端情况
在当前层直接把所有体积用完(不考虑后面的层),如果这样表面积仍然大于之前最优方案的值就cut
因为半径越大,单位体积需要的高度越小,而半径是随高度递增的
双向搜索
双向BFS,就是在起点和终点都很清楚的情况下,把起点和终点同时入队,或者进两个队,共同进行bfs,当二者第一次相遇时为最优解
这样做的目的是减轻bfs状态访问太多的症状
其实DFS也能双向,但是不好写
众所周知,通常情况下,搜索树越往下分叉越大,子节点可能呈指数级暴增,而上层状态相对较少
所以让两头共同搜索不仅仅是让时间减半,搜索树高度减半的结果可能是显著的
A*
盲目搜索与启发式搜索
前面提到的所有搜索都是都叫做盲目搜索,都是在遍历了所有状态后才能够判断是否可行。
在搜索时,有些状态成为正解的概率明显大于其他状态。启发式搜索就是用启发信息引导搜索。
启发信息
不同的启发信息有不同的强度。
当启发信息过强时,不能保证最优解。
当启发信息过弱时,优化效果不明显。
我们引入启发信息,显然既想要得到最优解,又想让程序跑得快。
所以就要合理设置启发信息。
A算法
定义评价函数,对当前的搜索状态进行评估,找出有希望的节点扩展,这就叫做A算法(启发式搜索算法A)
通常,A算法的评价函数形如\(f(n)=g(n)+h(n)\),其中\(f(n)\)是评价函数,\(g(x)\)是\(n\)已经用掉的开销,\(h(n)\)是一个启发式函数
我们还需要定义一些符号:
s、t:分别表示搜索的起点和目标
g(n):从s到n的最小代价
h(n):从n到t的最小代价
f(n)=g(n)+h*(n):从s经过n到t的最小代价
实际上g(n)、h(n)、f(n)分别是g(n)、h(n)、f*(n)的估计值
A算法还包含两个数据结构,叫做closed表和opend表
closed表里储存的是已经访问过的节点
opend表里储存的是当前能访问到,但是还没有访问的节点
其实就是一个堆+bool数组的事情,但是这种说法还是不少,可能在其他领域有作用
A算法具体流程
首先把出发点加入opend表里
然后进行bfs循环,每次循环找出opend表中f函数最小的状态,然后遍历此节点能够访问的所有状态
若被被遍历到的状态在closed表里根据A算法的特性,它们不必再被访问,若被遍历到的状态在opend表里,也可以无视他们,若遍历到的状态不在任何表里,则放入opend表中
直至访问到目标状态即可结束循环
A*算法
A*算法为保证\(h(x)<=h*(x)\)的A算法。具体体现在A算法不一定能够得到最优解,而A*算法一定能够得到最优解。(生活中主要使用A算法,使启发信息很强来保证效率和一定的质量)
例题
八数码问题
若要用A*算法解决问题,最关键的就是设计h函数
在保证h(n)<=h*(n)(最优解)的情况下让h(n)函数尽量大(跑得块)
h1
令h(n)=在n状态下,不在自己应该在的位置上的数字数
这样h(n)显然不大于h*(n),因为每次交换位置的是空格和数字,不可能把两个数字同时归位,要让所有数字归位,移动步数至少为不在位的数字数h2
我们可以发现,对于一个不在位置上的数,不仅至少要走一格让它回去,而且是至少要走当前位置到应在位置的曼哈顿距离
因为一次只能移动一格,而且一次不能同时移动两个数
所以我们可以把h(n)改成在状态n下,所有不在位的数字到目标位置的曼哈顿距离之和
注意空格不在统计的范围内
这样我们就设计了h函数,先不用管它是否是尽可能地大,但是至少比h(n)=0强,我们的程序已经得到优化了
IDA*
IDA*中的ID就是迭代加深的ID,A*还是那个A*
IDA*的核心依然是一个评价函数f(n)=g(n)+h(n)
不过这次利用它的方法为如果当前状态的评价函数f(n)大于迭代加深规定的搜索深度,就立刻回溯,不再深入
通俗地说就是我们通过设计,发现某些状态n到终点至少要走h(n)步,而实际要走的不会比h(n)少
如果g(n)+h(n)超过规定深度,那么无论如何状态n都不会搜到目标状态,可果断剪枝