OI 界有句话叫做“暴力打满”,即一次比赛拿到所有的暴力的分,考虑到在 NOIp/CSP 中暴力分较多,况且常数优化和暴力的一些剪枝也可以拿到更多分(而且像普及组 T1/T2 以及提高组的 Day1 T1 撰写正解难度并不高),拿到省一其实希望很大。
但是暴力也要找准方向,纯粹的枚举是愚蠢的,那么,让我们开始吧。
深度优先搜索(缩写 DFS)是对一个连通图进行遍历的算法。
其思想是从一个顶点
这样可以按顺序遍历所有的点,时间复杂度较高,所以称为暴力搜索,我们通常采用递归的形式编写程序,这样的搜索可以通过剪枝优化,从而使重复的搜索减少。
【例题】P1596 [USACO10OCT]Lake Counting S
void dfs(int x,int y){
a[x][y]='.';
int dx,dy;
for(int i=-1;i<=1;i++)for(int j=-1;j<=1;j++){
dx=x+i;
dy=y+j;
if(dx>=0&&dx<=n&&dy>=0&&dy<m&&a[dx][dy]=='W')dfs(dx,dy);
}return;
}
有一些基础的题目在洛谷没有,所以我创建并改良了几道类模板题供大家练手,详见此处。
我们首先访问顶点
这时候我们需要利用队列来保证我们访问节点的顺序,此时我们的效率比深搜大大提高,原因是我们能够省去很多的重复步骤,程序架构如下:
void bfs(){
定义队列 q;
q.push(初始状态);
while(队不空){
x(y)=队头状态信息;
出队;
if(下一步可行)下一步;
}
}
【例题】P1451 求细胞数量
struct grid{
int x,y;
};
queue<struct grid> q;
void BFS(){
cnt++;
while(!q.empty()){
int x=q.front().x;
int y=q.front().y;
vis[x][y]=1;
q.pop();
for(int i=0;i<4;i++){
int xx=x+dx[i],yy=y+dy[i];
if(yy<m&&xx<n&&xx>=0&&yy>=0){
if(!vis[xx][yy]&&p[xx][yy]!='0'){
q.push({xx,yy});
}
}
}
}
return;
}
【例题】AT1350 深さ優先探索
struct grid{
int x,y;
};
bool check(int x,int y){
return (!vis[x][y]&&x>=1&&x<=n&&y>=1&&y<=m);
}
void bfs(){
queue<grid>q;
q.push({x,y});
while(!q.empty()){
int xx=q.front().x,yy=q.front().y;
q.pop();
vis[xx][yy]=1;
if(check(xx+1,yy))q.push({xx+1,yy});
if(check(xx,yy+1))q.push({xx,yy+1});
if(check(xx-1,yy))q.push({xx-1,yy});
if(check(xx,yy-1))q.push({xx,yy-1});
}
}
常言道:记搜+剪枝可以解决一切问题
记忆化搜索实际上是递归来实现的,但是递归的过程中有许多的结果是被反复计算的,这样会大大降低算法的执行效率。
记忆化搜索:
而记忆化搜索是在递归的过程中,将已经计算出来的结果保存起来,当之后的计算用到的时候直接取出结果,避免重复运算,因此极大的提高了算法的效率。
剪枝:
当目前状态和题意不符,并且由于题目可以推出,往后的所有情况和题意都不符,那么就可以进行剪枝,直接把这种情况及后续的所有情况判断不可行,直接返回。
当几个字树具有完全相同的效果的时候,只选择其中一个搜索。
在我们用搜索方法解决最优化问题的时候的一种常用剪枝。当搜索还未结束时,记录的状态已经比当前保存的最优解更劣,那么此方案一定无效,停止搜索并回溯即可。
普遍来讲,搜索的顺序是不固定的,对一个问题来说,算法可以进入搜索树的任意的一个子节点。
但假如我们要搜索一个最小值,而非要从最大值存在的那个节点开搜,就可能存在搜索到最后才出解。我们从最小的节点开搜很可能马上就出解。这就是顺序剪枝的一个应用。一般来讲,有单调性存在的搜索问题可以和贪心思想结合,进行顺序剪枝。
此剪枝一定优先按照题目要求!
这一份题单在暴力搜索的同时也有部分剪枝和优化内容。