昨天上午日常考试差点爆零(也差不多啦~ QAQ)
先说说这三个题题意
T1:状态压缩BFS
T2:状态压缩DP
T3:①模拟栈②链表可加并查集优化(给柳畅神犇(Loier们の赤い太陽)跪烂Orz)
只想写第一个题的题解
不是因为我懒(实际却是主因),是因为T2还不会(求好心神犇解救),决定等开DP坑的时候再来填,T3字符串删除回文子串,当时打了一个n2暴力打次了TLE,都怪晚上睡太晚(早上),第二天脑子抽了只想着暴力解法了
如果用栈模拟的话简直水
话说这三道题是去年55级莱芜一中跟青岛二中胡策时的一套,在去年李元豪学长270接近AK
果然神犇就是神犇,抽空找神犇学状压DP
说在前面:推荐看完这道题去做CODE[VS] 2594 解药还是毒药(爽死)
A 迷宫(maze2.pas/c/cpp)
TL:1S ML:128MB
【Description】
你所在的迷宫可以用 N 行 M 列的矩阵来描述:
图标 含义
’ # ‘墙,无法通过
’ . ‘地面,可以通过
小写字母(a、b、c、…、z) 钥匙,可以打开标有对应大写字母的门
大写字母(A、B、C、…、Z) 门,可以被标有对应小写字母的钥匙打开
$ 你的初始位置
& 迷宫的出口位置
迷宫的四周都有墙,所以你无法走出这片 N*M 的区域,只能从”&”处离开迷宫
你只能向东西南北四个方向走。
你的任务是,计算出走出迷宫需要的最少步数是多少?
【Input】
第 1 行两个正整数 N 和 M,表示迷宫的长和宽;
第 2 行一个正整数 P,表示门和钥匙的数量;
第 3 行至第 N+2 行,描述整个迷宫。
【Output】
一个整数,为走出迷宫需要的最少步数如果不可能走出迷宫输出-1。
【Sample Input】
5 5
1
&A..$
####.
…#.
.#.#.
a#…
【Sample Output】
28
【Sample Input】
1 4
1
&aA$
【Sample Output】
-1
【Hint】
对于 30%:1 <= N, M <= 15
另外存在 10%:P=0
另外存在 10%:P=1
另外存在 20%:P=2
对于 100%:1<=N,M<=50,0<=P<=10,保证迷宫中”$”、”&”和所有大小写字
母只出现一次,钥匙和门的标号只会出现字母表中的前 P 个字母。
对于30%的数据暴力应该可以(但是不确定P到底是几这个东西应该意义不大)
对于另外10%,P = 0,直接跑bfs即可
再有10%,p = 1,跑一遍bfs,同时可以建一个bool型变量,记录是否拿到钥匙
另外20%可以依照上面的方法再加一个bool
怎么拿部分分介绍就到这里,现在我们来想怎样拿全100分
首先正解写在博客标题上
首先题面告诉我们,P<=10,这确保了数据不会太大而爆掉
其次,我们必须想一个方法来记录到达每个点时的状态,有钥匙还是没钥匙,有那把钥匙。
但是这些状态不是单独的,我们必须同时记录同一时刻处于某个点时的所有状态
数据范围决定了,开个超多维数组或是开上好几个大数组十分不现实= =
然后我们可以看到100%数据范围
1<=N,M<=50,0<=P<=10
一看这个不超过三位的数据范围,一看就是状压DP的数据范围
但这道题不能用DP来做(没法做),而应该是在图上跑BFS,因此正解为状压BFS
然后我们该怎么做
在此之前先说下什么是状态压缩
状态压缩,字面意思就是将许多状态压缩到一起
看到这个,大家心里肯定是
WFT???!!!
这TM怎么做,状态能压缩???该怎么压缩???
我在一开始接触状态压缩的时候也是这个样子,根本就是一脸懵逼(๑°ㅁ°๑)
很多神犇写的东西都相当简短,初读只能浮于表面(所以说明我根本就是个蒟蒻啦_ (:3 」∠)_)
压缩状态,就是把本来好多维的状态压缩到一维或是少维,在十进制下这几乎是不可能的事情,所以我们应该用到二进制和位运算来优化,利用二进制压位可以大大优化空间复杂度
在二进制中,第i位代表着十进制中2的i-1次方,这样一来便可以用二进制来表示出所有的整数
然后再用字典序来记录钥匙位置,这样一来算法逐渐浮现:
即是建立一个三维的bool数组,前两维用来记录当前坐标,第三维用来记录每种钥匙
具体的做法是,因为P<=10,所以第三维的大小应至少为1 << 10 == 1024
为什么要把数组kai,因为一共最多有10把钥匙,每把钥匙都有两种状态,有或是没有,所以总状态数即2^10种,因为1 << 10 == 2^10,所以我们应该把数组开到1 << 10这么大
每当搜到一个小写字母的时候,用< cctype >库自带的islower判断是否为小写字母,同样判断是否为大写字母我们可用同一个库中的isupper。
在函数后边括号内,直接写上你所存图中当前位置即可(map[i][j]),但是提醒一点,islower和isupper为宏定义,并非真正的函数,宏定义是不参与计算或者求解的,它所做的仅仅是替换,比如字符串替换等等
(虽然好像并没有什么影响,但还是想说一下)
然后当搜到一个小写字母的时候,如何记录呢,我们可以利用 -‘a’,来获得该字母的字典序,然后在三维bool数组的第三维上将1 << 该字母的字典序位,例如b的字典序为2,所以我们应该将1 << 2,以此类推,然后再将之前钥匙数按位或(|)1来得到当前的钥匙数(|,或运算符,x|y 只要x,y中一个为1,则x|y == 1,有且只有 0|0 = 0 )
然后我们在判断BFS是否出界的时候可以顺便判断一下,如果当前位置有门那么是否拥有可以开启这扇门的钥匙,因为大小写字母的字典序相同,所以只要再找出当前这扇门的字典序,再 1 >> 此门字母的字典序 再按位与(&)1看是否有钥匙可以开门即可
(关于位运算的话,请各位自行百度“位运算”啦(〃∀〃)ノ再说各位神犇肯定早就会了吧233,如果实在是不理解的话可以与容斥原理结合起来更加方便理解)
所以说:
状态压缩即将一个数拆分成它的二进制数,然后在这个二进制数上进行位运算来代表其他数以此来不同的代表状态,降低了维度,减少了对空间使用
只要搞明白了状态压缩,不论是状压DP还是状压BFS,还是别的什么什么,都差不多了呢
代码如下:
#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
const int maxn = 56;
using namespace std;
struct QWQ{
int x,y;
int key,dist;
};//用结构体封装更好看些
int n,m,p;
int qx,qy;
char map[maxn][maxn];
bool osu[maxn][maxn][1024];
int dx[5] = {1,-1,0,0};
int dy[5] = {0,0,-1,1};
bool out(int x,int y,int key){
if(x < 0 || x >= n||y < 0 || y >= m)
return false;
if(map[x][y] == '#'||osu[x][y][key])
return false;
if(isupper(map[x][y])){
int k = map[x][y] - 'A';
if(!((key >> k) & 1))
return false;
}
return true;
}
queue<QWQ > q;
inline int bfs(QWQ be){
be.key = 0;
be.dist = 0;
q.push(be);
osu[be.x][be.y][be.key] = true;
while(!q.empty()){
QWQ now = q.front();
q.pop();
if(map[now.x][now.y] == '&'){
printf("%d\n",now.dist);
return 0;
}
for(int i = 0;i < 4;i ++){
QWQ v = now;
v.x += dx[i];
v.y += dy[i];
v.dist++;
if(islower(map[v.x][v.y])){
int k = map[v.x][v.y]-'a';
v.key |= (1 << k);
}
if(!out(v.x,v.y,v.key))
continue;
osu[v.x][v.y][v.key] = true;
q.push(v);
}
}
printf("-1\n");
return 0;
}
int main(){
//freopen("maze2.in","r",stdin);
//freopen("maze2.out","w",stdout);
scanf("%d%d",&n,&m);
scanf("%d",&p);
QWQ begin;
for(int i = 0;i < n;i++){
for(int j = 0;j < m;j++){
cin>>map[i][j];
}
}
for(int i = 0;i < n;i++){
for(int j = 0;j < m;j++){
if(map[i][j] == '$'){
begin.x = i;
begin.y = j;
}
}
}
bfs(begin);
//fclose(stdin);
//fclose(stdout);
return 0;
}
我不做人啦Doge!
THE END
By Peacefuldoge