搜索专题总结
目录
一、 搜索算法
a) 深度优先搜索(dfs)
b) 广度优先搜索(bfs)
c) 迭代加深搜索(ID)
d) 启发式搜索(A*)
二、 剪枝与优化
a)深度优先搜索剪枝
1、最优性剪枝
2、可行性剪枝
3、搜索顺序
4、记忆化搜索
b)广度优先搜索剪枝
1、双向广搜
2、判重方法
1)hash表
2)STL大法
c)迭代加深搜索优化
IDA*
d)其他类型剪枝与优化
1、状态压缩
1)二进制优化
A空间优化
B时间优化
2)其他进制优化
2、数据处理
1)离线数据
2)预处理数据
一、 搜索算法
a)深度优先搜索(dfs)
(1) 思想:(试探法)从一条路往前走,能进则进,不能进则退回来,换一条路再试 像走迷宫一样
(2) 实现方法:递归,用栈保存
(3) 优缺点:
①优点:程序简单,容易上手,空间小,容易调试
②缺点:呆滞,求最优路时速度慢
(4) 经典例题:
①素数环:把1到20排成一个环,使得环中每俩相邻数字的和是个质数
②八皇后:八个皇后放在一个8×8的棋盘上,不可互相攻击
(5)模板:
Pseudo code:
void dfs(int x){
if(x>tie){ 已达到目标状态
print();
return ; 停止
}
for(int i=1;i=n;i++)
if(bo[i]){
bo[i]=0; 标记
dfs(x+1); 深搜
bo[i]=1; 取消标记
}
return ; 回溯
}
b)广度优先搜索(bfs)
(1) 思想:(走一步再走一步)层层扩展,先将当前所有点的下一层扩展,再将扩展后的点再次扩展……像传染病传染一样
(2) 实现方法:像削金字塔一样一层一层地遍历,用队列保存
(3) 优缺点:
①优点:时间小,找最优路时比深搜不知道好到哪里去了;
②缺点:空间巨大,编程较为复杂,调试很麻烦
(4) 经典例题:八数码水题:给出3×3的方格,留出一个空格,给出起始状态与目标状态,求最小操作数量
(5) 模板:
Pseudo code:
int bfs(){
初始化……
queue <node> q
q.push(a);
while(!q.empty()){
node now=q.front();
q.pop();
for(…){
node next=now;
if( next 符合目标状态)return next.walawala;
对next进行扩展;
q.push(next);
}
}
return -1;
}
int main(){
cin>>…;
cout<<bfs();
return 0;
}
c)迭代加深搜索(ID)
(1) 思想:(折返跑)深搜,
若没搜到答案,则减小限制,扩大搜索层数上线
(2) *实现方法:*dfs+一个if
(3) 优缺点:①优点:结合深搜与广搜的特点,空间小,时间小(ˇˍˇ) ,编程简单,简明易懂,是您居家旅行,暴力骗分的必备技能
②缺点:实在找不到缺点,太完美了,(⊙o⊙)哦,有一个很勉强的,就是加深后,前面的东东重复搜索了
(4) 经典例题:埃及分数:将一个分数分解为+++…+;并使n尽可能小,当解有多个时,取sn最小时的解
(5) 模板:
Pseudo code:
int constraint,deep=…;
bool dfs(int x){
if(x>constraint)return true;
if(有解){bo=1;return false;}
+dfs模板
}
int main(){
cin>>…;
while(dfs())constraint;
cout<<answer;
return 0;
}
d)启发式搜索(A*)
(1) 思想:(贪心)每次取当前最优点扩展;
(2) 实现方法:每次取当前扩展但未处理的点中预估值最高的,进行处理并扩展该点的连接点,将扩展的点放入优先队列,再从优先队列中取值;
(3)优缺点:①优点:搜索平均时间快,考场暴力骗分利器;
②缺点:搜索不稳定,容易被卡,且最优解不稳定;
(4) 实现过程:
1、把起点加入到堆中
2、重复以下步骤
a、从堆中找出F最小的节点,并把它当做当前的操作节点
b、检查当前点周围的点,如果已经在堆中看是否能通过当前点得到更小的G,如果能就更新那个点的G,F的值,如果在set中或者是障碍物(不可达)则忽略他们
c、把当前点从堆中移除 ,加入set中
d、当目标点加入set中时停止
3、保存路径,从目标点出发,按照父节点指针遍历,直到找到起点。
(5) 经典例题:八数码难题:题目见上;
(5)模板:
Pseudo code:
int get_h(int qq,int ee){
return ……;
}
struct node{
int a,b;
int walawala;
int h=get_h(a,b);
bool operator < (const int &x)const{ 以估价函数为关键字进行排序,大根堆
return x.h>h;
}
}
int bfs(){
初始化……
priority_queue <node> q
set <node> c;
q.push(a);
while(!q.empty()){
node now=q.front();
q.pop();
for(…){
node next=now;
if( next 符合目标状态)return next.walawala;
对next进行扩展;
if(!set.count(next)){
q.push(next);
c.insert(next);
}
}
}
return -1;
}
int main(){
cin>>…;
cout<<bfs();
return 0;
}
(7) 估价函数: A*的灵魂所在,估价函数写得好,分数翻番,写错了就扑街了
F(n)=g(n)+h(n)
g(n):从起点到n的距离
h(n):从n到终点的估价函数
常用估价函数:
①曼哈顿距离:h=|t.x-now.x|+|t.y-now.y|
网格中最常用的估价函数,保证不会剪到手,但效果较弱
②切比雪夫距离:h=max(|t.x-now.x|,|t.y-now.y|)
在国际象棋中国王式走法常用
其他类型的估价函数需根据题意自己设计,估价函数应朝着与现实数据接近的方向设计,当然,在一些特殊情况下,可另行设计(例如打暴力时若数据过大,则可使用强势的估价函数,剪去大部分枝强势骗分;反正就算估价正确也TLE
二、 剪枝与优化
a) 深度优先搜索剪枝(dfs)
1、 最优性剪枝:
(1) 思想:当目前状况已超过搜到的最优值,则返回
(2) 升级版:当目前状况+当前与目标的最小差值已超过最优值,则返回
(3) 适用范围:求最优值,且无负权值
(4) 注意事项:估价函数只可偏小不可偏大,且要求函数效率高
(5) 特点:结果绝对正确,剪枝效果适中,副作用较小
2、可行性剪枝:
(1) 思想:当目前状况已不满足于题意,则返回
(2) 升级版:每次仅判断当前拓展状况可能影响到的条件
(3) 适用范围:有限定条件的题
(4) 特点:结果绝对正确,剪枝效果适中,若不用升级版,则副作用可能极大
(5)*经典例题:*Betsy的旅行:给出n×n的方格,求(1,1)到(n,n)的全遍历路线数
利用思想(1):对于一条合法的路径,除出发点和目标格子外,每一个中间格子都必然有“一进一出”的过程。必须保证每个尚未经过的格子都与至少两个尚未经过的格子相邻(除非当时Betsy就在它旁边)。
但是:假如在每次剪枝判断时,都简单的对N2个格子进行一遍扫描,其效率的低下可想而知。因此,我们必须尽可能的简化判断的过程。
所以利用升级版:由于Betsy的每一次移动,只会影响到附近的格子,所以每次判断时,应当只对其附近的格子进行检查:即只通过对Betsy附近的格子进行判断,就确定是否应当剪枝
3、 搜索顺序:
(1) 思想:利用题目特征,判断搜索优先顺序,把可能得到解得分枝放在前边搜
(2) 升级版:其实就是A*
(3) 适用范围:只要你能分析好题目特征,都可以用
(4) 注意事项:千万别用这个求最优解(除非你在暴力骗分)
(5) 特点:
①若求可行解:超厉害的说(虽然没A*神),只要分析到位了,时间超短,空间也小,剪枝效果不稳定,平均情况较好,副作用小
②若求最优解:不是奔着暴力分去尽量别用,在搜完前无法证明得到的解是最优解
4、 记忆化搜索:
(1) 思想:在搜索时记录当前节点以后的搜索最优值,当搜索时发现当前节点已经被记录时(若当前状态不比节点储存值更优)直接调用,节约时间
(2) 升级版:当记忆化达到一定程度时,或搜索十分有规律,可进化为“动态规划”
(3) 适用范围:不同搜索路径不会改变已储存值的准确性(例如:每个点仅能走一次,并且不是树)或对一个点调用多次(可及时更新,一定程度上还是节约了时间的)
(4) 注意事项:一定要保证节点数据恒为正确或数据更新及时
(5) 特点:当对一棵树多次搜索或对一张稠密图进行搜索时效果明显,几乎无副作用;当节点数据不能恒为正确时需多次更新,效果较弱;若图退化成链或树且仅搜索一次,则无优化效果(并有那么一丁儿点的小小的常数时间)
(6) 经典例题:滑雪:给一张图上所有点数值,求图中所含的最长下降路径
分析:若采用暴力算法,对每一个点进行dfs,取maxx
但由于对于每一个点,以其为起点的最长下降路径可以确定,可以保证节点保存的数据恒为正确,若用记忆化搜索,每个点最多搜索一次,时间降为O(n)
b) 广优先搜索剪枝(bfs)
1、双向广搜:
(1) 思想:已知起始状态与目标状态,则从起始状态与目标状态分别广搜,每次分别仅扩展一层,并与对面的新扩展层进行比较,若有相同值则输出双方权值和
(2) 优缺点:
①优点(与bfs相比):速度较快,空间较小
②缺点:编码较为复杂,判重较为复杂
(3) 适用范围:已知起始状态与目标状态,求从起始状态到目标状态的最小值
(4) 注意事项:每次bfs仅扩展一层
(5) 模板:
Pseudo code:
int bfs(){
初始化……
queue <node> q1,q2;
q1.push(a);q2.push(b);
int step1=0,step2=0;
while(!q.empty()&&!q.empty()){
扩展一层q1,判断是否与q2最外层有相同值 若有 return step1+step2;
step1++;
扩展一层q2,判断是否与q1最外层有相同值 若有 return step1+step2;
step2++;
}
return -1;
}
int main(){
cin>>…;
cout<<bfs();
return 0;
}
2、判重方法:
(1)Hash表
1 思想: 以数组为储存空间,对状态进行高效判断是否存在
2 实现方法:数组+hash函数
3 优缺点:查找时间相比于STL较优,但空间没有STL那样有弹性
4 几种常用数字hash函数:
A取余法:
a思想:对一个大数mod p(p为质数),作为地址
b优缺点:优点:代码极其简单,p可以自己设置,空间与准确度之间可自己取舍
缺点:数字大起来准确度不高
B随机化法:
a思想:对于一个数t,首先用取余法求出t的hash函数中的某些值,再利用这些值进行hash运算
b优缺点:优点:不会被坑爹数据坑到,肯定没人能故意卡它
缺点:但也许会自己挂掉
C平方切割法:
a思想:对一个数t进行平方,再取中间几位作为地址
优缺点:优点:准确度高
缺点:要求数字不得太高
D康拓展开:
a思想:当一些数进行全排列时,进行数字分析,减小空间开销
b适用范围:仅全排列时可用
c公式:X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0!
其中a[i]表示:对于该全排列的第i位上的数字,在它的右边的位数上有a[i]个数字比它小。
d优缺点:优点:100%避免冲突
缺点:时间复杂度高:O(n^2)//还不如用set
5、关于字符串hash函数:
A转换法
a公式:hash[i]=(hash[i-1]*p+idx(s[i]))%mod
b优缺点:优点:貌似找不出
缺点:有点随机,冲突可能性较高,空间无弹性
B查找树(其实不是hash)
a思想:将字符串建树,进行搜索(单词查找树)
b优缺点:优点:时间优,空间小
缺点:程序复杂
C STL大法好
(2) STL大法
1 map
a用法:存储:将存在过的值进行map映射到一个特殊值上
查询:时若映射值等于特殊值则存在过
b优缺点:
优点:可以设特殊值以直接找到存在的位置,代码简单且啥数据类型都可以
缺点:速度贼慢
2 set
a用法:存储:set<…>c;c.insert(要存储的东东);
查询:c.count(要查的东东)若为1则存在
b优缺点:优点:代码简单,速度较快,数据随意,空间有弹性
缺点:速度较hash较慢
c) 迭代加深搜索优化(IDA*)
(1) 思想: ID时用A*
(2) 适用范围: 能用ID就能用IDA*
(3) 优缺点:
优点:空间小,时间小,用了都说好
缺点:代码稍复杂
(4) 模板:
Pseudo code:
int get_h(int qq,int ee){
return ……;
}
struct node{
int a,b;
int walawala;
int h=get_h(a,b);
bool operator < (const int &x)const{ 以估价函数为关键字进行排序,大根堆
return x.h>h;
}
}
int constraint,deep=…;
int dfs(int x){
初始化……
priority_queue <node> q
set <node> c;
q.push(a);
while(!q.empty()){
node now=q.front();
if(now.walawala>constraint)return -1;
q.pop();
for(…){
node next=now;
if( next 符合目标状态)return next.walawala;
对next进行扩展;
if(!set.count(next)){
q.push(next);
c.insert(next);
}
}
}
return -1;
}
int main(){
cin>>…;
while(ans==-1)ans=dfs(),constraint+=deep;
cout<<answer;
return 0;
}
(5) 经典例题:十五数码:题目与八数码类似,不过是4×4的方格
d) 其他类型剪枝与优化
1、 状态压缩
1) 二进制优化
A空间优化:
1 思想:利用计算机二进制保存的特点,用一个整形变量保存数十个bool变量,整形变量二进制的第i位上为1即bo[i]=1;
2 优缺点:优点:空间满载时可优化成原来的1/8,运算速度快
缺点:代码难以构思,状态单一,难以编译
3 适用范围:当保存量很大且保存类型为bool时才可用
4 例题:胜利大逃亡(续):有k1把钥匙,k2扇门,求最短路
分析:若用bool数组保存钥匙状态,则空间开销较大,且结构体转移时速度较慢;但一旦用了二进制优化,现在头不昏眼不花,空间小了,也顺带少了一点常数时间
B时间优化:
1 思想:利用位运算那强大的常数时间,节约时间
2 优缺点:优点:运用到极致则有奇效(例题中会说明)
缺点:一般想不到二进制时间优化
3 适用范围:想到并证明正确即可使用
4 经典例题:n皇后问题:参照八皇后,#define ‘八’‘n’
分析:若采用传统dfs方法进行广搜,即便利用可行性剪枝升级版也会在n较大时扑街,所以可以考虑用二进制保存每一行能否放置当前皇后,设置l,ld,rd分别保存该列,左斜线,右斜线上是否被攻击,并在dfs下一层时ld<<1,rd>>1,这样就以迅雷不及掩耳之势找到了当前行能放置皇后的点,并免去了状态转移的n次运算或是可行性剪枝时较大的副作用
2) 其他进制优化
1 思想:利用n进制的储存,减小空间开销,用时间换空间
2 优缺点:
优点:空间开销减小程度大大的说,因为不用进制优化,至少也要用short,腻大了
缺点:速度没了二进制的支持,甚至比O(1)还慢
3 例题:在一张地图上,每个点最多经过俩次
分析:只要时间够,空间爆了,便可用三进制用时间换空间
2、 数据处理
1)离线数据
1 思想:利用考试喝茶的时间,放一个程序打出与输入无关但要用的万能数据以节约时间跑暴力
2 升级版:人送外号 ~(~ ̄▽ ̄)~ 打表 ~( ̄▽ ̄~)~
3 适用范围:预处理的数据必须与输入无关
4 注意事项:
A要先算好程序运行时间,以防快考完了还没跑完
B要算好数据量,以防源程序过大系统直接pass//亲身经历
┑( ̄Д  ̄)┍
5 特点:百分百节约时间
6 经典例题:亚瑟王的宫殿:多个骑士与一个国王在n×m方格上,要求最终所有人到达同一点,(国王骑士走法参照国际象棋)骑士可以接国王,俩人在一起时步数算一人,求最小总步数
解析:可以离线处理好从(1,1)到(maxn,maxm)的骑士步数,方便以后调用
2) 预处理数据
1 思想:防止一个测试点中多组输入数据处理时重复计算
2 适用范围:一个共有数据,多组分数据
3 注意事项:要先算好程序运行时间,以防预处理占用大多数时间,并且当组数少并且预处理
繁杂时不宜使用
4 特点:当数据组数较多时很节约时间,但当组数少并且预处理繁杂时很坑
5 经典例题:亚瑟王的宫殿:见上
解析:可以预先处理好从(1,1)到(i,j)的骑士步数,方便以后调用(这样较离线处理也许可以保证源程序的空间不爆)