问题描述:
来源:“蓝桥杯”练习系统
X星球的流行宠物是青蛙,一般有两种颜色:白色和黑色。
X星球的居民喜欢把它们放在一排茶杯里,这样可以观察它们跳来跳去。
如下图,有一排杯子,左边的一个是空着的,右边的杯子,每个里边有一只青蛙。
∗
*
∗WWWBBB
其中,W字母表示白色青蛙,B表示黑色青蛙,*表示空杯子。
X星的青蛙很有些癖好,它们只做3个动作之一:
1. 跳到相邻的空杯子里。
2. 隔着1只其它的青蛙(随便什么颜色)跳到空杯子里。
3. 隔着2只其它的青蛙(随便什么颜色)跳到空杯子里。
对于上图的局面,只要1步,就可跳成下图局面:
WWW
∗
*
∗BBB
本题的任务就是已知初始局面,询问至少需要几步,才能跳成另一个目标局面。
输入为2行,2个串,表示初始局面和目标局面。
输出要求为一个整数,表示至少需要多少步的青蛙跳。
输入输出:
样例输入
∗
*
∗WWBB
WWBB
∗
*
∗
样例输出
2
样例输入
WWW
∗
*
∗BBB
BBB
∗
*
∗WWW
样例输出
10
数据规模和约定:
我们约定,输入的串的长度不超过15
资源约定:
峰值内存消耗(含虚拟机) < 256M
CPU消耗 < 1000ms
思考:
在完成这道题之前,虽然我做了很多搜索的题目,包括暴力搜索,DFS,BFS等等,但是终究没有十分深刻理解搜索算法的内涵。为什么该类算法编程简单,一个通用的内核递归外套不同情况的外壳能够胜任大多数题目?为什么回溯法改进了暴力枚举,大大提升了算法的效率?DFS和BFS的区分是什么,分别该怎么写?其实,巧妙的来源都是大家耳熟能详的东西,递归和空间换时间。这也正说明递归和空间换时间的果是因为这些需求和算法改进的因得到的。
首先,一般来说DFS适合找到一条符合题目要求的路径但可能不是最优的路径。为什么呢?深度优先搜索是一种枚举完所有完整路径以遍历所有情况的搜索方法。虽然,DFS看起来用了递归,划分了分支,但最终就是把所有的情况列一遍,代码看起来好看,但实际思想是丑陋的,给CPU带来了很多压力。于是,很多人选择找到一些限定条件来给他剪枝,以便减少运算量。好,回到最初的问题。为什么DFS不适合找到最优路径?这是因为找到一条路径的条件很宽松,但是找到最优的话是必须遍历几乎所有情况的。
当初在学BFS和DFS的时候,我被书上铺天盖地的经典案例所迷惑了。像什么N皇后问题,背包问题等等。当时,看不懂为什么一个棋盘上N皇后遍历居然靠一排数字纠结了很久。还有,因为背包问题,误以为DFS是以一个个背包为深度搜索一个个情况。甚至,为了理解BFS去看了看“泛洪”是啥?算法跟数学还是不太一样,很多数学定理公式还是具备很多几何意义的,算法更像把实际生活中的解决问题的方法进行抽象。递归来自于一个人在路上开车经历多个路口进行重复的判断,空间换时间有点像把所有可能的选择列出来挑选最优的,而不是一层一层进行选择,失败了可能回头重来。
接着,我们回到BFS。在学习BFS的时候,书上是这么说的,广度优先搜索一般采用队列实现,且总按层次顺序进行遍历。我一开始以为BFS和DFS没有相关性,因为采用BFS算法的解答是基本上不使用递归的。但是,实际上很多人将DFS和BFS是混用的,这真是让人迷惑。对于这一问题,我的回答是,BFS仍然是有递归思想的,不过递归本质上也是枚举,只是采取了优雅的方式,而BFS更进一步采用的队列实则是一种空间换时间的方式。很多人使用BFS的时候怎么用的?除了队列,有时还用set,有的还用vector存着。很多解释BFS的时候采用广度搜索,我感觉是一种误解,因为一但这么解释将会大大缩小其适用范围,毕竟它大多数情况不是这么用的。说白了,根据我的理解,BFS也只是将所有情况列举出来而已,进行不重复筛查也是剪枝的一种。那为什么BFS总是最优解呢?BFS遍历的方式是采用层,这对应最优路径的step,邻接矩阵的块数。BFS是遍历DFS的前几层,而答案一般要求的最优就是最少,自然一找到就退出的限定也保证了BFS找到的必然是最优。
代码1:
这个代码是最初的雏形,我写的虽然是BFS实际上是DFS,筛查的条件很简单,一但超过12步就停下,这显然不适合所有,有点像蹭分的操作,居然得了1/3的分。不过,通过这个错误案例也证明了DFS是全枚举的事实,有限的剪枝是解决不了问题的。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<set>
#include<queue>
#include<algorithm>
#include<cmath>
using namespace std;
char s[20],f[20];
int minstep=12;
int len;
bool check(char* s1,char* s2)
{
for(int i=0;i<strlen(s1);i++)
if(s1[i]!=s2[i]) return false;
return true;
}
int kongbei(char* s)
{
for(int i=0;i<strlen(s);i++)
if(s[i]=='*') return i;
}
void bfs(int k,int step)
{
if(step>minstep) return ;
if(check(s,f)==true)
{
minstep=step;
return ;
}
for(int i=k-3;i<k+4;i++)
{
if(i<0||i>len-1) continue;
swap(s[i],s[k]);
bfs(i,step+1);
swap(s[i],s[k]);
}
}
int main()
{
cin>>s>>f;
len=strlen(f);
bfs(kongbei(s),0);
cout<<minstep;
return 0;
}
代码2:
这次我改进采用了书上通用的BFS公式,但是结果仍然不理想。本来想借用set去重的功能,但是check的时候增加了很多的计算量,导致部分超时,只得了66。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<set>
#include<queue>
#include<map>
#include<algorithm>
#include<cmath>
using namespace std;
string s,f;
int minstep;
set<string> qset;
struct Node
{
string str;
int step;
int kb;
Node(string ts,int tkb,int tstep)
{
step=tstep;
kb=tkb;
str=ts;
}
};
int kongbei(string ts)
{
for(int i=0;i<ts.length();i++)
if(ts[i]=='*') return i;
}
bool check(string ts)//检查是否已经存在
{
for(set<string>::iterator it=qset.begin();it!=qset.end();it++)
{
if(ts==*it) return true;
}
return false;
}
void bfs()
{
queue<Node> q;
q.push(Node(s,kongbei(s),0));
while(!q.empty())
{
Node top=q.front();
q.pop();
if(top.str==f)
{
minstep=top.step;
return ;
}
int len=top.str.length();
int k=top.kb;
for(int i=k-3;i<k+4;i++)
{
if(i<0||i>len-1) continue;
string t=top.str;
swap(t[i],t[k]);
if(!check(t))
{
qset.insert(t);
q.push(Node(t,i,top.step+1));
}
}
}
}
int main()
{
cin>>s>>f;
bfs();
cout<<minstep;
return 0;
}
代码3:
这是最终满分的代码。这次的改进来自网上另一位老哥分享的代码,他使用了map作为查重的依据。很棒的改进,这也说明它参透了BFS的本质,而不是人云亦云用set得到错误结果。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<set>
#include<queue>
#include<map>
#include<algorithm>
#include<cmath>
using namespace std;
string s,f;
int minstep;
set<string> qset;
map<string,int> qmap;
struct Node
{
string str;
int step;
int kb;
Node(string ts,int tkb,int tstep)
{
step=tstep;
kb=tkb;
str=ts;
}
};
int kongbei(string ts)
{
for(int i=0;i<ts.length();i++)
if(ts[i]=='*') return i;
}
bool check(string ts)//检查是否已经存在
{
for(set<string>::iterator it=qset.begin();it!=qset.end();it++)
{
if(ts==*it) return true;
}
return false;
}
void bfs()
{
queue<Node> q;
q.push(Node(s,kongbei(s),0));
while(!q.empty())
{
Node top=q.front();
q.pop();
if(top.str==f)
{
minstep=top.step;
return ;
}
int len=top.str.length();
int k=top.kb;
for(int i=k-3;i<k+4;i++)
{
if(i<0||i>len-1) continue;
string t=top.str;
swap(t[i],t[k]);
/*if(!check(t))
{
qset.insert(t);
q.push(Node(t,i,top.step+1));
}*/
if(!qmap[t])
{
qmap[t]=1;
q.push(Node(t,kongbei(t),top.step+1));
}
}
}
}
int main()
{
cin>>s>>f;
bfs();
cout<<minstep;
return 0;
}
最后贴出我的测评结果。后面内存的使用量的增加,也是空间换时间的体现。同时,相较于借鉴的那位老哥,为什么我的代码更优秀?依据题意青蛙只能跳入杯中且距离有限。我通过这点只遍历空杯前后两只以内的青蛙,实现了剪枝,并非核心算法更优。