POJ3278/洛谷P1588 catch that cow 题解
1. 对搜索的介绍
搜索是算法竞赛,公司面试中都必不可少的一个技能。刚开始学习的搜索那一定是最经典的BFS(广度优先搜索)和DFS(深度优先搜索)。
BFS就相当于彻底的暴力法,走遍所有的路径,那必能遇到我想要的结果,或者证明一个结果不存在。
DFS相当于“不撞南墙不回头”,一定要一条路走到底,通常这种搜索方式用于寻找“最深”,“最短”等路径。
1.1 剪枝
在进行搜索时,通常会多次进入同一个状况,这样大大增加了代码运行的时间,并且做的还是无用功。
例如本题中,人先往 左 走,再往 右 走 和 先往 右 走,再往 左 走,结局都是回到了原点。如此循环往复,搜索所需的时间将是巨大的。所以需要剪枝,目的就是去除这些重复劳动。
同时,如果一个情况只有一个处理方法,那也不必去搜索,直接通过那种方法得到结果即可,这样也算剪枝。
2. 广度优先搜索的思路
2.1 队列
在开始讲解广度优先搜索之前,需要先知道 队列 这个数据结构。
队列,顾名思义,就像人们排的队一样的数据结构。先进队列的人先出来,后进队列的人等前面的人都走完了才能出来。
队列有多种实现方式,可以使用CPP中的STL中的queue,也可以自行手写。在这里给出一种我从 @罗勇军 老师 PPT中学到的极简的队列
const int MAX_N=1e5; //挑一个合适的大小
int que[MAX_N],head,tail
head++; //队列头被弹出 但注意head不能比tail大
//上下两行按需交换
queue[head]; //这里处理队列头
que[++tail]=data; //数据入队
当然这样的队列还是有缺陷的,比如如果数据量过大,队列就不够塞了。或者如果一个节点需要有多个数据,那就需要改成结构体。
2.2 BFS 的实现方法
有了队列,那我们在遇到BFS时候就有了一个强大的工具。
再回顾一下BFS,广度优先搜索,为了实现走完每一条道路,一般通过“逐层扫荡”的方式。也就是,我从一个情况出发,然后接下来会有多种情况的选择,那我就都走,到了再下一个情况,我依然有有多种情况可以选,直到我遇到了我想要的情况,或者所有情况都走完了,也没遇到,那说明无解。
现在有了队列,我们可以在处理完一个情况后,把它接下来跟着的多个情况塞入队列。这样可以在处理完同一层情况后再处理下一层情况,实现所有的节点都被“逐层扫荡”。
2.3 剪枝的方法
去除那些重复的或者多余的情况,最常用的方法就是Hash。
2.3.1 Hash
简单的来说 hash就是一种用空间换取时间的一种方式。
举个例子:给1-10000中20个随机数 排序,可能最快的方法就是用Hash。
方法:先开一个数组A[10005]={0},然后从第一个随机数开始扫,比如是26,那就将A[26]=1; 下一个是303,那就将A[303]=1; 如果再下一个又是26,那就将A[26]=2;直到扫完为止。
然后用以下方法输出
for(int i=1;i<=10005;i++)
if(A[i]>0){
for(int j=1;j<=A[i];j++)
printf("%d ",i)
}
这种做法复杂度基本就是O(1)。在处理得到的数字进入数组的时候已经天然完成了排序。
但缺点就在于,如果数字太多太大,就需要开很大的数组,甚至可能不够。
2.3.2 Hash 剪枝思路
根据上一个例子可以知道 可以用 0 和 非0 做区分,那可以将经历过的状态标1,那这样在处理每一个情况时,先去判断是否为0,不为0就不处理了。
在处理完之后,把这个对应的状态标1。 这样就可以实现“不走重复的路”。
3. 对此题的分析
这道题是典型的搜索题。
本题可以理解为:有一个人要走到一个地方,一步走法有一下三种选择:1.可以往左走一格 2.可以往右走一格 3.可以到2*现在所在格子的地方。
也就是说,我现在在X的地方,我下一步可以在X-1,X+1,2X的地方。
现在问,如何最快到达我想去的地方。
利用2.2讲的思路,一开始的队头就是起点所在的位置,队头出去后,入队的就是起点+1,起点-1,2*起点
显然,本题不可能无解,因为就算只靠 +1 和 -1 也总能到达目的地。
3.1 此题的剪枝
从最简单的开始,如果一开始目的地就在起点的左边,那只有不断的-1才能最快到达目的地(+1和*2都属于绕远路,不可取)。
第二种便是Hash剪枝,由于起点,目的地都处于同一根数轴,那用一个数组就能实现Hash。一开始数组全标0,然后一旦遇到一个新情况,处理完就标1。
如果此题不进行剪枝,那将会一直运行下去。因为在不断的+1与-1后会回到原地,所以必须要剪枝。
4. 代码
#include <bits/stdc++.h>
using namespace std;
struct node{
int level; //计算走了几步
int num; //用于记录这个情况在数轴上所处于的位置
};
int visit[200009]; //用于Hash
queue<node>q; //用于BFS的队列
int minpath(int now,int aim){ //用于计算最少步数
if(now==aim) return 0; //起点终点位于同一地点的情况
if(now>aim) return now-aim; //终点在起点右边,只能不断-1的情况
struct node nows,nexts;
nows.level=0;nows.num=now; //起点
while(!q.empty()) q.pop(); //因为可能多次运行该函数,先清空
q.push(nows); //起点入队
while(!q.empty()){ //运行至队列无元素
if(q.front().num==aim) return q.front().level; //函数结束的条件
nows=q.front();
q.pop();
if(visit[nows.num])continue; //利用Hash 如果非0就做下一个情况
visit[nows.num]=1; //标1,以后就不进来了
nexts.level=nows.level+1; //+1的情况
nexts.num=nows.num+1;
q.push(nexts);
nexts.num=nows.num-1; //-1的情况
q.push(nexts);
nexts.num=nows.num*2; //*2的情况
if(nexts.num<=aim*2)
//如果我现在已经远大于目标所在位置,那不用入队,
//因为一定可以通过直接-1最快速到达。
q.push(nexts);
}
}
主函数部分
int main(){
int now,aim,a; //a表示循环次数(相当于多道题目)
scanf("%d",&a);
for(int i=1;i<=a;i++){
scanf("%d %d",&now,&aim);
memset(visit,0,sizeof(visit)); //全设为0
int ans=minpath(now,aim);
printf("%d\n",ans);
}
return 0;
}
经测试,平均每个测试点都花费50ms左右的时间。
5. 总结
对于像我一样的算法初学者而言,理解这类题的思路并不难,但一旦上手操作,便会不知所措,所以要勤加练习。
有人说,教别人是最好的提升自己水平的过程。因为可以从中找到自己不完善的地方。于是我就写下了这篇,也是我的第一篇文章(顺便能交个作业)。
如果我这篇文章写的哪里有问题,欢迎私聊。