1.题目描述:点击打开链接
2.解题思路:本题是一道很经典的路径寻找问题,利用A*算法解决。即BFS+利用估价函数剪枝。做本题的第一个障碍是题意比较复杂,需要事先在草稿纸上理清头绪,弄清楚这4种操作,每种操作又分哪些特殊情况。还要弄清楚4种冰块是怎么转换的。这样,弄清楚这些细节,才能够写出鲁棒的扩展状态的函数。
接下来,就是如何利用BFS来寻找路径了。首先要弄清楚怎么保存一个状态,应该记录哪些我们关心的量。在本题中,由于输入的是一个字符串,只要将他们排成一列,即可完整的描述出当前的一个状态,因此我们用字符串的形式来保存一个状态。由于最终要的不是最小的步数,而是一系列操作,因此这里的d数组保存的应该是实现最短步数的一串操作,即字符串->字符串(普通的BFS是“状态”->整数)。这里我们不再用d数组,改用sol(solution的缩写)来存储,还可以知道sol应该是一个map类型。BFS还需要一个queue来实现,由于状态是字符串,即queue<string>q。
接下来就是确定BFS的主体框架,这点其实也不难,为了更清楚的表示状态的扩展,我们可以专门写一个expand函数来实现,这样,大致框架应该如下:
while(!q.empty())
{
string s=q.front();
q.pop();
if(expand(s,'<'))break;
if(expand(s,'>'))break;
if(expand(s,'L'))break;
if(expand(s,'R'))break;
}
其中,q我们定义为全局变量,这样可以在expand函数中加入新的状态。同时,我们把expand定义为bool型,这样,当找到解时候,输出并return true即可,其他情况return false。这样显得框架非常清晰直观。
接下来的重点就是根据题意写好expand函数了,详细的过程请参考代码注释,不予赘述。由于我们引入估价函数进行剪枝,因此有必要确定好估价函数h(),在本题中,很明显应该是当前状态至少还要走多少步才能到达目标点作为估价函数的指标,题目中特意提到15步之内一定有解,因此如果cur+h(cur)>15,就可以进行剪枝了。这里的cur应该是当前操作的长度。另外,由于还涉及到下落操作,为了更方便的处理,我们在操作完指令后另外写一个fall函数来表示下落的过程(具体细节见代码注释),这样,最终的状态就是新扩展出来的状态。判重操作也变得非常简单:如果sol中没有新的状态s,就入队列。
if(!sol.count(s))
{
sol[s]=seq;
q.push(s);
}
本题中,值得学习就是常量数组的使用来简化操作:link_r,link_l,clear_l,clear_r,icy。还有将复杂的操作给分解,专门编写相应的函数进行处理,使得框架变得简单清晰。另外,状态的确定和路径的确定也是本题值得学习的一点。
3.代码:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<algorithm>
#include<string>
#include<sstream>
#include<set>
#include<vector>
#include<stack>
#include<map>
#include<queue>
#include<deque>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<ctime>
#include<cctype>
#include<functional>
using namespace std;
#define me(s) memset(s,0,sizeof(s))
#define pb push_back
typedef long long ll;
typedef unsigned int uint;
typedef unsigned long long ull;
typedef pair <int, int> P;
const int N=256;
int n,m,target; //行数,列数和目标格的编号(从上到下编号为0~nm-1)
map<string,string>sol; //sol[s]表示从初始状态到达状态s的最短操作序列
queue<string>q; //BFS用的状态队列
bool icy[N];
char link_l[N],link_r[N],clear_l[N],clear_r[N];
void init()//初始化常量数组
{
memset(icy,0,sizeof(icy));//icy[i]表示字符i是否为冰
icy['O']=icy['[']=icy[']']=icy['=']=true;
memset(link_l,' ',sizeof(link_l)); //link_l[c]表示将字符c往左连接后的新字符
link_l['O']=']';link_l['[']='=';
memset(link_r,' ',sizeof(link_r)); //link_r[c]表示将字符c往右连接后的新字符
link_r['O']='[';link_r[']']='=';
memset(clear_l,' ',sizeof(clear_l)); //clear_l[c]表示将c左端的字符清除后的新字符
clear_l[']']='O';clear_l['=']='[';clear_l['O']='O';clear_l['[']='[';
memset(clear_r,' ',sizeof(clear_r)); //clear_r[c]表示将c右端的字符清除后的新字符
clear_r['[']='O';clear_r['=']=']';clear_r['O']='O';clear_r[']']=']';
}
string fall(string s)//让状态中的悬空的冰块和冰人落地
{
int k,r,p;
for(int i=n-1;i>=0;i--) //由于是落地操作,要从下往上寻找
for(int j=0;j<m;j++)//从左往右寻找
{
char ch=s[i*m+j];
if(ch=='O'||ch=='@')//如果是独立的冰块或冰人
{
for(k=i+1;k<n;k++)if(s[k*m+j]!='.')break;
s[i*m+j]='.';s[(k-1)*m+j]=ch;
}
else if(ch=='[')//“冰棍”的左端
{
for(r=j+1;r<m;r++)if(s[i*m+r]=='X'||s[i*m+r]==']')break; //寻找冰棍的右端,试图得到冰棍[j...r]
if(s[i*m+r]==']') //是一个完整的“冰棍”,可以下落,如果连接的右端是石头就不能下落
{
for(k=i+1;k<n;k++) //在i+1,i+2,...,n-1行寻找支撑点
{
bool found=false;
for(p=j;p<=r;p++)if(s[k*m+p]!='.'){found=true;break;}
if(found)break;//找到了支撑点(k,p)
}
for(p=j;p<=r;p++)s[i*m+p]='.'; //冰棍原来的所有地方都变为空格
for(p=j+1;p<r;p++)s[(k-1)*m+p]='='; //下落后的位置,冰棍中间部分都是无自由端的冰
s[(k-1)*m+j]='[';s[(k-1)*m+r]=']'; //确定左右两端冰的类型
}
j=r; //j直接跳到冰棍的右端,开始新的寻找
}
}
return s; //返回下落后的新状态
}
int h(string s)//估价函数,确定从当前状态s到达目标点还有至少多少步
{
int a,b,x=s.find('@');
a=x%m-target%m; if(a<0)a=-a; //确定横向距离a
if(x/m>target/m)b=x/m-target/m; //如果目标点在高处,那么至少还有走b步
else b=(x/m<target/m?1:0);//如果等高,b=0, //如果目标点在低处,最快就是一步跳下去,b=1
return a>b?a:b; //返回较大者
}
bool expand(string s,char cmd)//扩展出当前状态s执行cmd后的新状态,如果找到了解,返回true
{
string seq=sol[s]+cmd;//新状态的操作序列,等价于经典的BFS中的最短步数
int x=s.find('@'); //找到冰人的位置
s[x]='.'; //暂时视为冰人离开了原地
if(cmd=='<'||cmd=='>') //施魔法操作
{
s[x]='@';
int p=(cmd=='<'?x+m-1:x+m+1);//魔法作用的障碍格子的编号
if(s[p]=='X')return false; //不能对石头施法
else if(s[p]=='.') //目标是空地,可以变成冰
{
s[p]='O';
if(icy[s[p-1]])s[p-1]=link_r[s[p-1]];//如果p左端是冰,那么p-1处的冰块向右连接
if(s[p-1]!='.')s[p]=link_l[s[p]]; //如果p左边是冰或者石头,p向左连接
if(icy[s[p+1]])s[p+1]=link_l[s[p+1]];//同上
if(s[p+1]!='.')s[p]=link_r[s[p]]; //同上
}
else //目标是冰,可以变为空地
{
s[p]='.';
if(icy[s[p-1]])s[p-1]=clear_r[s[p-1]]; //如果p左边是冰,拆除p-1处冰块向右的连接
if(icy[s[p+1]])s[p+1]=clear_l[s[p+1]]; //同上
}
}
else //移动操作
{
int p=(cmd=='L'?x-1:x+1); //移动目标
if(s[p]=='.')s[p]='@'; //目标是空地,直接走过去
else
{
if(s[p]=='O') //目标地是独立冰,尝试把它推走,注意:此时冰人原地不动
{
int k;
if(cmd=='L'&&s[p-1]=='.')//往左推
{
for(k=p-1;k>0;k--)if(s[k-1]!='.'||s[k+m]=='.')break;
s[p]='.';s[k]='O';s[x]='@';
}
if(cmd=='R'&&s[p+1]=='.')//往后推
{
for(k=p+1;k<n*m;k++)if(s[k+1]!='.'||s[k+m]=='.')break;
s[p]='.';s[k]='O';s[x]='@';
}
}
if(s[p]!='.') //遇到障碍,或者独立冰没有被推走,往上爬
{
if(s[p-m]=='.'&&s[x-m]=='.')s[p-m]='@';
else s[x]='@';
}
}
}
s=fall(s); //悬空冰块和冰人往下落
if(h(s)+seq.length()>15)return false; //最优性剪枝
if(s.find('@')==target) //找到解
{
printf("%s\n",seq.c_str());
return true;
}
if(!sol.count(s)) //判重操作
{
sol[s]=seq;
q.push(s);
}
return false;
}
int main()
{
int rnd=0;
init(); //初始化常量数组
while(~scanf("%d%d",&n,&m))
{
if(!n)break;
char mp[20][20];
for(int i=0;i<n;i++)
scanf("%s",mp[i]);
string s="";
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
{
if(mp[i][j]=='#')
{
target=i*m+j;
mp[i][j]='.';
}
s+=mp[i][j];
}
q.push(s);
sol.clear();
sol[s]="";
printf("Case %d: ",++rnd);
while(!q.empty())
{
string s=q.front();
q.pop();
if(expand(s,'<'))break;
if(expand(s,'>'))break;
if(expand(s,'L'))break;
if(expand(s,'R'))break;
}
while(!q.empty())q.pop();
}
}