搜索学习笔记+杂题 (基础一 简单的dfs+bfs)
搜索杂题:
博客中讲述的题的题单:戳我
一、基础的BFS与DFS:
深搜和广搜都可以遍历出在一定限制下可能出现的所有情况,但是朴素的搜索一般复杂度极高,成指数级别,需要用到各种五花八门的优化方式,后面会一一介绍,但基础很重要,几乎不用考虑优化,直接模拟题意就可以了。这篇博文讲的是习题ing。
深搜一般处理有分支的情况,广搜一般解决图上的问题。
(1)深搜:
深度优先搜索算法(英语:Depth-First-Search,简称DFS)。
是一种用于遍历或搜索树或图的算法。沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。
这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。属于盲目搜索。
简要概括,深度优先的主要思想就是“不撞南墙不回头”,“一条路走到黑”,如果遇到“墙”或者“无路可走”时再去走下一条路。
P2392 kkksc03考前临时抱佛脚:
显然是深搜,由于左右大脑可以处理同样科目的两个问题,对于每个科目都进行一次深搜,找出每个科目需要的最少时间,那么加起来就是总的最少时间。
那么定义
关键代码:
inline void dfs(int id,int num)//分别代表这是哪一个学科,现在是这个学科的第几个作业
{
if(num>s[id])//超过这个学科的作业了
{
minn=min(minn,max(lx,rx));
return ;
}
lx+=a[id][num];//放左脑
dfs(id,num+1);
lx-=a[id][num];//撤销
rx+=a[id][num];//放右脑
dfs(id,num+1);
rx-=a[id][num];//撤销
}
P1036 [NOIP2002 普及组] 选数:
建议学一下素数筛,预处理使用素数筛将1e7以内的素数全部筛出来,复杂度
定义深搜
关键代码:
inline void pre()//线性筛的板子(要学自己搜,或许等下辈子我会写)
{
for(int i=2;i<=sum;i++)
{
if(!a[i]) prime[++cnt]=i;
for(int j=1;j<=cnt&&prime[j]*i<=sum;j++)
{
a[prime[j]*i]=1;
if(i%prime[j]==0) break;
}
}
}
inline void dfs(int u,int sum,int maxx)
{
if(u==m)//已经有m个数了
{
if(!a[sum]) ans++;//是素数就累加
}
for(int i=1;i<=n;i++)
{
if(!vis[i]&&i>maxx)//没有用过这个数&&从前向后搜避免重复
{
vis[i]=1;//用过了
dfs(u+1,sum+x[i],i);
vis[i]=0;//撤销,下一个状态
}
}
}
P2036 [COCI 2008/2009 #2] PERKET:
题意很显然,唯一新颖的就是多了一个必须要加调料的限制和酸度是各个调料乘在一起。深搜写起来要好做一点。
定义深搜
注意的是当前物品要选就将
关键代码
dfs(u+1,sa,sb,flag);//不选
dfs(u+1,sa*p[u].a,sb+p[u].b,1);//要选
P1101 单词方阵
稍微会麻烦一点的题,由于各个方向都可以,共有八个方向上,左上,左,左下,下,右下,右,右上。而且已经给出了需要找的目标串
将整个地图输入,显然,要匹配成功,前两个字符肯定是必须要存在的,于是我们可以遍历图中的
这时候用深搜,判断以某一点为起点,朝着某一个方向,是否存在目标串,定义深搜状态
关键代码:
inline void dfs(int sx,int sy,int x,int y,int num)//上面说了意思
{
if(num==7)
{
flag=1;return ;//匹配成功
}
if(s[x+sx][y+sy]!=a[num+1]) return ;//匹配失败,及时止损
else dfs(sx,sy,x+sx,y+sy,num+1);
}
P2404 自然数的拆分问题:
顶针深搜,非常入门的一道题,从前向后搜就可以了,可以保证是按字典序的大小来的。
定义
关键代码:
for(int i=qs;i<=n-1;i++)
{
a[c]=i;
dfs(he+i,c+1,i);
a[c]=0;
}
P1596 [USACO10OCT] Lake Counting S:
采用深搜和广搜都有的一个小trick,染色法。输入map之后,一个个遍历,找到一个字符"W"且这个位置没有被遍历的时候,答案+1,然后从这个点深搜,找出所有与这个点联通的水坑,联通的水坑又深搜的找它联通的水坑,这样向四周扩展,打上标记,然后最后统计答案。
当然这道题广搜也同样可以做,也是可以运用染色的思想。定义
关键代码:
for(int i=1;i<=8;i++)
{
int sx=x+fx[i],sy=y+fy[i];(其中fx,fy是方向数组,懒得一个一个方向写)
if(!vis[sx][sy]&&mapp[sx][sy])//我是预处理了的,如果这里是水坑,mapp[i][j]就为1
{
dfs(sx,sy);//深搜
}
}
P1294 高手去散步:
题面可能有一点绕,需要的前置知识就是邻接矩阵存图(简单地说,如果1点到2点有一条长度为5的边,那么mapp[1][2]=5,通式是mapp[u][v]=w,代表从u到w有一条长度为w的边,邻接矩阵非常占空间,一般处理点数少于2000的图)。
存好图,一次枚举从
关键代码:
inline void dfs(int u,int len)//哪个点了,现在走了多远
{
ans=max(ans,len);//全局变量记录最大的路程
for(int i=1;i<=n;i++)//邻接矩阵存图,看当前点的所有出边
{
if(!vis[i]&&mapp[u][i]>0)//没有到过&有边
{
vis[i]=1;//到过了
dfs(i,mapp[u][i]+len);//去i点,长度加上mapp[u][i],即u至i的距离
vis[i]=0;//撤销操作,搜下一个状态
}
}
}
P1605 迷宫
艹,我还以为这道题我是用广搜做的,结果发现自己是用深搜做的。由于从起点开始,有障碍物,那么从(1,1)开始,除开障碍物,向四周延展,思路比较简单,建议写一个方向数组(为什么当时我没写)方便一点,边界就是终点,每到一次终点就是一种不同的路径,数据范围让你不用担心超时,所以暴力一个个加就可以了。
关键代码:
inline void dfs(int x,int y)//坐标,展开之后相当好理解
{
if(x==fx&&y==fy)//到终点了
{
ans++;
return ;
}
if(x+1<=n&&!vis[x+1][y]&&!mapp[x+1][y])
{
vis[x][y]=1;
dfs(x+1,y);
vis[x][y]=0;
}
if(x-1>=1&&!vis[x-1][y]&&!mapp[x-1][y])
{
vis[x][y]=1;
dfs(x-1,y);
vis[x][y]=0;
}
if(y+1<=m&&!vis[x][y+1]&&!mapp[x][y+1])
{
vis[x][y]=1;
dfs(x,y+1);
vis[x][y]=0;
}
if(y-1>=1&&!vis[x][y-1]&&!mapp[x][y-1])
{
vis[x][y]=1;
dfs(x,y-1);
vis[x][y]=0;
}
}
CF377A Maze
灰常有意思的一道题,需要运用逆向思维,由于正向模拟似乎有点麻烦,而且要维护剩下的点是联通的,所以不好做。但是如果我们换一个角度,我们在输入的时候统计是点的个数,记作
因为一开始的点也都是联通的,于是我们利用深搜的性质,从任意一个点出发,向四周扩散,扩散
关键代码:
inline void dfs(int x,int y)
{
if(sum==res) return ;//到了给定点后就返回
vis[x][y]=1,sum++;//统计遍历了多少个点
for(int i=1;i<=4;i++)
{
int sx=x+fx[i],sy=y+fy[i];//方向数组
if(sx<1||sx>n||sy<1||sy>m||mapp[sx][sy]=='#'||vis[sx][sy]) continue;//超出边界和#以及已经走过的点不走
dfs(sx,sy);
}
}
P3915 树的分解
前置知识是树的遍历(不会的这道题可以先略过),深搜最大的用处之一就是遍历树,这道题是贪心,深搜的同时记录树的大小,一旦大小等于要求的值,答案就加一,最后判断答案与要求数目是否一样。
关键代码:
inline void dfs(int u,int fa)
{
siz[u]=1;
for(int i=head[u];i!=0;i=p[i].next)//使用链式前向星存图
{
int v=p[i].to;
if(v==fa) continue;//树是建的双向边
dfs(v,u);
if(siz[v]==k) siz[v]=0,num++;//子树大小满足要求,答案加1,子树贡献归0
siz[u]+=siz[v];
}
if(siz[u]==k) siz[u]=0,num++;//当前节点下的子树满足要求,答案加1,当前节点大小贡献归0
}
P2080 增进感情:
基础深搜题,按照题目所给的要求模拟搜索,每一个事件有做与不做两种可能。
关键代码:
inline void dfs(int u,int sa,int sb)//分别代表目前是第几个事件,小明的好感度,小红的好感度
{
if(u==n)//搜索边界,最后一个事件
{
if(sa+sb>=v)//模拟题意,好感度之和超过v
{
ans=1,minn=min(minn,abs(sa-sb));//说明有答案,记录最小差值
}
return ;
}
dfs(u+1,sa,sb);//不做这个事件
dfs(u+1,sa+a[u+1],sb+b[u+1]);//做这个事件
}
P1025 [NOIP2001 提高组] 数的划分
和前面有一道分解数的思想有点类似,但实际上这道题你可以写一个三重循环应该可以过(其实不配黄,它甚至不用存方案),由于本质不同,只用注意从前向后搜就是了,初始状态
关键代码:
inline void dfs(int u,int sum,int pre)
{
if(u==k&&sum==n)//抵达边界,同时满足u个数与和为n
{
//cout<<u<<" "<<sum<<" "<<pre<<"\n";
ans++;return ;//累加答案
}
for(int i=pre;i<=n;i++)
{
if(sum+i>n||u+1>k) break;
dfs(u+1,sum+i,i);//向后搜
}
}
P1219 [USACO1.5] 八皇后 Checker Challenge
深搜入门题中最经典的一道了,同时也是处理起来最烦的一道。要考虑斜方向的存在性,其中左下-右上比较好处理,因为对角线上的
有点恶心,所以给出全部代码以供调试:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=35;
int n,res=0;
int vis[M];//每一列
int x1[M];//↗和一样
int x2[M];//差一样
int ans[M];
inline void dfs(int u)
{
if(u>n)
{
res++;//记录方案数量
if(res<=3)//三个以内还要输出方案
{
for(int i=1;i<=n;i++)
{
cout<<ans[i]<<" ";//边搜边存
}
cout<<"\n";
}
}
for(int i=1;i<=n;i++)
{
if(!vis[i]&&!x1[u+i]&&!x2[u-i+15])
{
vis[i]=x1[u+i]=x2[u-i+15]=1;
ans[u]=i;
dfs(u+1);
vis[i]=x1[u+i]=x2[u-i+15]=0;//加15是因为当x比y小的时候不够减,需要+15使下标变成正的
}
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
dfs(1);
cout<<res<<"\n";
return 0;
}
(2)广搜:
首先访问初始点v并将其标志为已经访问。接着通过邻接关系将邻接点入队。然后每访问过一个顶点则出队。按照顺序,访问每一个顶点的所有未被访问过的顶点直到所有的顶点均被访问过。
广度优先遍历类似与层次遍历。其特点是尽可能先对横向进行搜索,从指的出发点,按照该点的路径长度由短到长的顺序访问图中各顶点。
利用队列先进先出的性质,从起点开始,将一步能到达的点全部存入队列,然后将队列中队首元素出队,执行与起点相同的操作,以此循环,直到到达终点或者队列为空,队列为空说明可以到达的点都已经遍历过了,保证遍历出可以到达的全部情况。
P7724 远古档案馆(Ancient Archive)&CF645A Amity Assessment
呃,有点恶心,分类讨论就可以了,也可以使用玄学的方法(详见讨论区)
P6566 [NOI Online #3 入门组] 观星
广搜板子题之一,但实际上处理起来还有点麻烦,会用到深搜里面提到过的染色法,将相连一片区域内的所有星星赋成同一个值,然后用桶排序,用两次,第一次求出每一个星座有多大,第二次记录同一个大小的星系有多少个,最后输出星系数量(也就是有多少种大小的星座)与最大的星系大小。
关键代码:
int fx[9]={0,1,-1,0,0,1,1,-1,-1};
int fy[9]={0,0,0,1,-1,1,-1,1,-1};//方向数组,存好八个方向
inline void bfs(int bx,int by,int col)//起点坐标+第几个星座了
{
queue<pair<int,int>> q;//广搜一眼都会借助队列,你可以手写队列(但不推荐,易错),c++的STL库挺好用的
q.push(make_pair(bx,by));//将起点入队
vis[bx][by]=col;//将起点染色
while(!q.empty())
{
int x=q.front().first,y=q.front().second;//取出在队列中的一个点,那这个点去更新其他点,如果队列空了就说明能更新的都更新了,所有情况都遍历完了,这时退出循环
q.pop();//把这个点弹出,代表这个点已经用来更新过其他点了
for(int i=1;i<=8;i++)
{
int sx=x+fx[i],sy=y+fy[i];
if(sx<1||sx>n||sy<1||sy>m||!mapp[sx][sy]) continue;//超边界或不是星星
if(!vis[sx][sy])//没有染过的(说明没有拿来更新其他点)
{
vis[sx][sy]=col;//染上颜色
q.push(make_pair(sx,sy));//加入队列,以便之后取出更新它周围的点
}
}
}
}
P1443 马的遍历
同样的,只不过方向不是之前那八个方位了,变成马走的方向了(日字)。本题会有一个新的概念——松弛操作,这种操作可以让我们找到到达目标最小的代价,一般与广搜+queue搭配使用。
松弛操作的操作步骤一般是先将距离数组
如果
需要判断一下
给一个这个松弛操作的代码:
queue<pair<int,int> >q;//由于是坐标嘛,存两维就用pair了
inline void bfs()
{
memset(dis,0x3f,sizeof(dis));//距离数组赋为极大值
q.push(make_pair(x,y));//x,y就是给出的起点,将起点先入队
dis[x][y]=0,vis[x][y]=1;//起点距离更新为0,标记起点已经入队
while(!q.empty())
{
int xx=q.front().first,yy=q.front().second;//取出一个点更新其他点
vis[x][y]=0;//取出的这个点就不再队列中了
q.pop();//弹出这个点,代表已经更新了这个点
for(int i=1;i<=8;i++)
{
int sx=xx+fx[i],sy=yy+fy[i];//目标状态
if(sx<1||sx>n||sy<1||sy>m) continue;//超出边界了 (有时候有其他限制,例如4墙之类的东西,一般在一个mapp数组上标记好哪里可走哪里不可走)
if(dis[sx][sy]>dis[xx][yy]+1)//就是dis[v]>dis[u]+w的变形
{
dis[sx][sy]=dis[xx][yy]+1;//可以变小就更新
if(!vis[sx][sy])//入队列了就不管,没有入队就入队
{
q.push(make_pair(sx,sy));
vis[sx][sy]=1;//标记入队了
}
}
}
}
}
由于给出了起点,将起点拉入队列,然后不断拉点更新就可以了,由于不能到达的输出-1,先将距离数组赋为极大值,然后如果这个点没有被松弛(值没有变小),就说明这个点没有被遍历过,判断一下就可以了。
说了这么多,这道例题总会了吧(上面的代码就是这道题的代码)
给一个方向数组(这道题要用日字)
int fx[9]={0,-1,-2,-2,-1,1,2,2,1};
int fy[9]={0,-2,-1,1,2,2,1,-1,-2};
P1135 奇怪的电梯
非常经典的广搜,它甚至只有一维坐标,跟着题意模拟,还是距离先
关键代码:
inline void bfs()
{
memset(dis,0x3f,sizeof(dis));//最大
queue<int> q;
q.push(a);dis[a]=0,vis[a]=1;//处理起点
while(!q.empty())
{
int x=q.front();q.pop();
vis[x]=0;
if(x+s[x]>=1&&x+s[x]<=n)//电梯向上跑 没超边界
{
if(dis[x+s[x]]>dis[x]+1)//可否更新(因为每次操作的代价为1)
{
dis[x+s[x]]=dis[x]+1;//更新
if(!vis[x+s[x]])//入队没有
{
q.push(x+s[x]);//入队
vis[x+s[x]]=1;//标记
}
}
}
if(x-s[x]>=1&&x-s[x]<=n)//电梯向下跑 其余同上
{
if(dis[x-s[x]]>dis[x]+1)
{
dis[x-s[x]]=dis[x]+1;
if(!vis[x-s[x]])
{
q.push(x-s[x]);
vis[x-s[x]]=1;
}
}
}
}
}
CF1063B Labyrinth
有限制:一方面是有不可以去的地方,另一方面左右操作是受限的。和上面两道一样,预处理好,起点入队。
那这道题的重点就在左右操作的限制上,定义两个数组
关键代码:
int fx[5]={0,1,-1,0,0};
int fy[5]={0,0,0,-1,1};
inline void bfs(int x,int y)
{
queue<pair<int,int> >q;
q.push(make_pair(x,y));//加入起点
col[x][y]=vis[x][y]=1;//染上颜色,同时标记入队
zx[x][y]=yx[x][y]=0;//起点肯定不需要任何左右操作
while(!q.empty())
{
int x=q.front().first,y=q.front().second;
q.pop();
vis[x][y]=0;//正常取出,点出队
for(int i=1;i<=4;i++)
{
int sx=x+fx[i],sy=y+fy[i];
int sl=zx[x][y]+(i==3),sr=yx[x][y]+(i==4);//i为3时就是向左,i为4时就是向右
if(mapp[sx][sy]||sx<1||sx>n||sy<1||sy>m||sl>lx||sr>rx) continue;//如果超出边界或操作超出限制就不搜了
if(zx[sx][sy]>sl||yx[sx][sy]>sr)//是否可以更新当前点,只要使向左操作或向右操作少一点都可以更新
{
zx[sx][sy]=sl,yx[sx][sy]=sr;
if(!vis[sx][sy])
{
vis[sx][sy]=col[sx][sy]=1;//染色+标记+入队
q.push(make_pair(sx,sy));
}
}
}
}
}
P1747 好奇怪的游戏
同马的遍历,其实考察的就是一个多测的清空,注意找到第一个的答案之后要将
关键代码:
if(sx<1||sx>50||sy<1||sy>50) continue;//边界开大一点
主函数里
cin>>x>>y;
bfs(x,y);
cout<<dis[1][1]<<"\n";
memset(vis,0,sizeof(vis)),memset(dis,0x3f,sizeof(dis));//清空,赋最大值预处理
cin>>x>>y;
bfs(x,y);
cout<<dis[1][1]<<"\n";
return 0;
P1141 01迷宫
小小的变式,只需要处理每一步跳到不同颜色的格子上就行了,由于可能存在几个连通块,所以使用染色法,搜每一个点,如果没有遍历过那就拿出来广搜,最后桶排,计算出每一个连通块(每一种颜色)的格子个数。
先处理完每个点,对于多次询问直接输出询问的点所在的连通块(颜色)的格子个数,不然每问一次扫一遍图明显会超时,已经要有基本的优化手段。
关键代码:
inline void bfs(int x,int y,int color)//起点坐标+颜色
{
queue<pair<int,int> > q;
q.push(make_pair(x,y));
vis[x][y]=1,col[x][y]=color;
while(!q.empty())
{
int x=q.front().first,y=q.front().second;
q.pop();
vis[x][y]=0;
for(int i=1;i<=4;i++)
{
int sx=x+fx[i],sy=y+fy[i];
if(sx<1||sx>n||sy<1||sy>n||mapp[x][y]==mapp[sx][sy]) continue;//我们只能到不同颜色的格子上去,所以mapp[x][y]==mapp[sx][sy]是不合法的,跳过
if(!col[sx][sy])
{
col[sx][sy]=color;//染上颜色
if(!vis[sx][sy]) q.push(make_pair(sx,sy));
}
}
}
}
主函数里
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(!col[i][j])//一旦这个点没有被遍历过,那就广搜出这个连通块
{
bfs(i,j,++cnt);//不同的颜色
}
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
t[col[i][j]]++;//颜色数量肯定很少,桶排各颜色格子数量
}
}
int x,y;
while(q--)
{
cin>>x>>y;
cout<<t[col[x][y]]<<"\n";//输出询问点所在颜色的大小
}
P1256 显示图像
板子题,只是起点很多,初始化
最后就是输出
关键代码:
inline void bfs()//入好队了
{
while(!q.empty())
{
int x=q.front().first,y=q.front().second;
q.pop();
vis[x][y]=0;
for(int i=1;i<=4;i++)
{
int sx=x+fx[i],sy=y+fy[i];
if(sx<1||sx>n||sy<1||sy>m||mapp[sx][sy]) continue;
if(dis[sx][sy]>dis[x][y]+1)//每一个距离代价就是1嘛,看可不可以更新
{
dis[sx][sy]=dis[x][y]+1;
if(!vis[sx][sy])
{
vis[sx][sy]=1;
q.push(make_pair(sx,sy));
}
}
}
}
}
主函数:
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(mapp[i][j]==1)//我将所有白色点对应的mapp值赋为1
{
dis[i][j]=0,q.push(make_pair(i,j));
vis[i][j]=1;//入队+赋dis为0+标记入队了
}
}
}
P1746 离开中山路
相比前面的题,这道题更像一道板子题,也是直接预处理,起点入队,广搜+松弛操作。最后输出终点对应的
关键代码:
inline void bfs()
{
while(!q.empty())
{
int x=q.front().first,y=q.front().second;
q.pop();
vis[x][y]=0;
for(int i=1;i<=4;i++)
{
int sx=x+fx[i],sy=y+fy[i];
if(sx<1||sx>n||sy<1||sy>n||mapp[sx][sy]) continue;//店铺的mapp值我赋的是1,如果是店铺就不可走,跳过
if(dis[sx][sy]>dis[x][y]+1)
{
dis[sx][sy]=dis[x][y]+1;
if(!vis[sx][sy])
{
vis[sx][sy]=1;
q.push(make_pair(sx,sy));
}
}
}
}
}//板中板
P2298 Mzc和男家丁的游戏
板中板+1,在输入的时候处理一下起点终点,顺便在mapp中标记出不可走的墙。
关键代码:
inline void bfs()
{
queue<pair<int,int> >q;
while(!q.empty())
{
int x=q.front().first,y=q.front().second;
q.pop();
vis[x][y]=0;
for(int i=1;i<=4;i++)
{
int sx=x+fx[i],sy=y+fy[i];
if(sx<1||sx>n||sy<1||sy>m||mapp[sx][sy]) continue;//超出边界||这是墙就跳过
if(dis[sx][sy]>dis[x][y]+1)
{
dis[sx][sy]=dis[x][y]+1;
if(!vis[sx][sy])
{
vis[sx][sy]=1;
q.push(make_pair(sx,sy));
}
}
}
}
}//板子题,不想讲
P1332 血色先锋队
也是前面的多倍经验,多个起点,多个询问。
关键代码:
inline void bfs()
{
while(!q.empty())
{
int x=q.front().first,y=q.front().second;
q.pop();
vis[x][y]=0;
for(int i=1;i<=4;i++)
{
int sx=fx[i]+x,sy=fy[i]+y;
if(sx<1||sx>n||sy<1||sy>m) continue;
if(dis[sx][sy]>dis[x][y]+1)
{
dis[sx][sy]=dis[x][y]+1;
if(!vis[sx][sy])
{
vis[sx][sy]=1;
q.push(make_pair(sx,sy));
}
}
}
}
}//最板的代码