HDU - 1429 的原题链接( http://acm.hdu.edu.cn/showproblem.php?pid=1429)
(一) 题目的大概思路
本题中我们要用到状态压缩的处理,将本题处理为一个朴素的广搜。本文参考了改博主的一些思想 谢谢该博主的思路
我们用到了哈希的思想。(二进制数中的0对应没有,1对应有)比如,假设我们有4种钥匙 A,B,C,D. 假设开始一把钥匙都没有,对应十进制数0, 在二进制的储存时,为0 0 0 0,对应一把钥匙都没有。
再比如有二进制数, 0 0 0 1,就好比 A,B,C,D中,我有A钥匙,没有B,C,D钥匙。因为正好二进制数0 0 0 1,有一个十进制数与其对应 ,再好比,我有钥匙A,B,C,但没有D,那我们就可以用 0 1 1 1,这个二进制数来对应,同时,又有一个十进制数的二进制储存正好为这个二进制数。所以这正好就形成了一种哈希。由于对于每一种钥匙 我们有 (有和没有)两种情况,故 我们至少需要 2n个十进制数来对应。 (比如 有4种钥匙 我们有0000,0001,0011…1111等16种情况)
(二)注意事项
这个题不同于朴素的广搜的原因是,因为有钥匙和门的存在,因为必须要有钥匙才能够开门。
当然,我们之前考虑到,每一个十进制数的二进制储存,恰好对应了我们手中的钥匙的状态,但实际上,我们并不能直接访问的对十进制数的二进制信息。那怎样才能够达到访问,或者修改二进制信息的目的呢?
恰好我们有位运算 ,当然,我们需要一点位运算的基础,所以我要感谢这名博主在此引用 他的博文。下面属于 直接copy这名博主的博文:
为了更好的理解状压dp,首先介绍位运算相关的知识。
1.’&’符号,x&y,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
2.’|’符号,x|y,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
3.’’符号,xy,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。例如3(11)^2(10)=1(01)。
4.’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。相应的,’>>’是右移操作,x>>1相当于给x/2,去掉x二进制下的最有一位。
这四种运算在状压dp中有着广泛的应用,常见的应用如下:
1.判断一个数字x二进制下第i位是不是等于1。(可以用于判断我们是否拥有对应门的钥匙)
方法:if ( ( ( 1 << ( i - 1 ) ) & x ) > 0)
将1左移i-1位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。
2.将一个数字x二进制下第i位更改成1。(可以让我们改变手中钥匙的状态)
方法:x = x | ( 1<<(i-1) )
证明方法与1类似,此处不再重复证明。
3.把一个数字二进制下最靠右的第一个1去掉。
方法:x=x&(x-1)
这样我们就能够处理门与钥匙了,因为我们看到门时,必须判断我们,手里是否有钥匙,只有拥有对应门钥匙我们才能够开门。同时,我们在找到 每一把钥匙的时候,我们必须更改我们手中钥匙的状态,比如开始我们手中没有一把钥匙,为0000,但我们找到A钥匙后,我们想变为0001,那么我们通过位运算就能够处理好了。
(三) 代码
#include <algorithm>
#include <queue>
#include <string.h>
#include <math.h>
#include <iostream>
#include <stdio.h>
#define ll long long
#define inf 0x3f3f3f3f
const int maxn = 27;
using namespace std;
int n,m,T;
int beginx,beginy,endx,endy; //begin 记录起点。 end 记录终点。
int d[4][2]= { {-1,0},{1,0},{0,1},{0,-1}}; //行走方向
struct Node
{
int x,y,key,step;
//key 这样十进制数用来哈希手中钥匙状态step用来记录步数
};
bool vis[maxn][maxn][1<<11]; //vis 用来标记,看是否走过
//虽然 A-J只有10把钥匙,但是保险开 2的11次方种情况
char mymap[maxn][maxn]; //记录我的图
bool check(Node next) //check 函数用来判断可不可以走
{
if(next.x<0||next.x>=n) return false; //判断是否越界
if(next.y<0||next.y>=m) return false; //判断是否越界
if(mymap[next.x][next.y]=='*') return false; //是障碍就退出
if(vis[next.x][next.y][next.key]) return false; //走过就退出
if(isupper(mymap[next.x][next.y]))
// ctype.h 头文件的一个函数,用来判断是否是字母大写
{
if(((1 <<(mymap[next.x][next.y]-'A')) & next.key ) == 0) //位运算,判断是否有钥匙
return false; //没钥匙就退出
}
return true;
}
void bfs()
{
memset(vis,0,sizeof(vis)); //很关键,,每次将标记归零,,血的教训
queue<Node>q;
q.push( {beginx,beginy,0,0} ); //把起点放进队列
vis[beginx][beginy][0]=true; //标记起点
while(!q.empty())
{
Node t= q.front();
q.pop();
if(t.x==endx && t.y==endy) //到达终点结束
{
if(t.step >= T) printf("-1\n"); //超时
else printf("%d\n", t.step); //逃出
return;
}
for(int i=0; i<4; i++)
{
Node temp = {t.x+d[i][0],t.y+d[i][1] ,t.key,t.step+1}; //上下左右都走一遍,步数+1
if( islower(mymap[temp.x][temp.y]))
// ctype.h 头文件的一个函数,用来判断是否是字母小写
{
temp.key|= 1 << (mymap[temp.x][temp.y]-'a');
// 位运算,改变钥匙状态,添加这把钥匙
}
if(check(temp))
{
vis[temp.x][temp.y][temp.key]=true;
q.push(temp);
}
}
}
printf("-1\n"); //找不到终点的非正常情况的输出
}
int main()
{
while(scanf("%d%d%d",&n,&m,&T)!=EOF)
{
for(int i=0; i<n; i++)
{
getchar();
for(int j=0; j<m; j++)
{
mymap[i][j]=getchar(); //存图
if(mymap[i][j]=='@') beginx=i,beginy=j; //记录起点
else if(mymap[i][j]=='^') endx=i,endy=j; //记录终点
}
}
bfs();
}
return 0;
}