题目描述
小 H 在一个划分成了 n×m 个方格的长方形封锁线上。 每次他能向上下左右四个方向移动一格(当然小 H 不可以静止不动), 但不能离开封锁线,否则就被打死了。 刚开始时他有满血 66 点,每移动一格他要消耗 11 点血量。一旦小 H 的血量降到 00, 他将死去。 他可以沿路通过拾取鼠标(什么鬼。。。)来补满血量。只要他走到有鼠标的格子,他不需要任何时间即可拾取。格子上的鼠标可以瞬间补满,所以每次经过这个格子都有鼠标。就算到了某个有鼠标的格子才死去, 他也不能通过拾取鼠标补满 HP。 即使在家门口死去, 他也不能算完成任务回到家中。
地图上有五种格子:
0
:障碍物。
1
:空地, 小 H 可以自由行走。
2
:小 H 出发点, 也是一片空地。
3
:小 H 的家。
4
:有鼠标在上面的空地。
小 H 能否安全回家?如果能, 最短需要多长时间呢?
输入格式
第一行两个整数 n,m, 表示地图的大小为 n \times mn×m。
下面 nn 行, 每行 m 个数字来描述地图。
输出格式
一行, 若小 H 不能回家, 输出 -1
,否则输出他回家所需最短时间。
输入输出样例
输入 #1复制
3 3 2 1 1 1 1 0 1 1 3
输出 #1复制
4
说明/提示
对于所有数据,1≤n,m≤9。
2021.9.2 增添一组 hack 数据 by @囧仙
思路简介:
虽然这是一道明显的BFS题,但是用DFS也能解,但DFS很容易TLE和WA。这是因为DFS是不撞南墙不回头的,如果常数操作设计的不够精炼,很容易由于过度深入而导致TLE。所以要想用DFS AC掉此题,常数操作一定要精简,所以一定要尝试去除不必要的常数操作。常熟操作其实就是指与数据无关的操作,比如比较操作,判断操作,运算操作等等。而其实时间复杂度也就是由常数操作决定的。AC代码及注释如下。
#include<bits/stdc++.h>
using namespace std;
int n, m;
int stx, sty;
int a[10][10];
int ans[10][10][7];//用来存每个状态下的到达此点的最优解
int min1 = 0x7ffff;//一开始把最小值设置的较大
int wk[4][2] = { {1,0},{0,1},{0,-1},{-1,0} };//能走的上下左右四个方位
void dfs (int now, int x, int y,int life) {//now现在的步数,x,y为现在的坐标,life为现在的血量
if (a[x][y]==3) {//到达终点
min1 = min(min1, now);//取较小值
return;
}
if (a[x][y] == 4)life = 6;//回血
if (x<1 || x>n || y<1 || y>m || now >=min1 || a[x][y] == 0 || life <=1 || now >=ans[x][y][life])
return;//如果越界或now已经>=min1,或遇到障碍,或生命值已经<=1,或now>=到达此点的暂时的最小值 便不用再扩展搜索此点
ans[x][y][life] = min(ans[x][y][life], now);//取较小值
for (int i = 0; i < 4; i++) {//四个方位搜索
int tx = x + wk[i][0];
int ty = y + wk[i][1];
dfs(now + 1, tx, ty, life - 1);
}
}
int main(){
cin >> n >> m;
memset(ans, 0x3f, sizeof(ans));//初始化一个较大的值
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
if (a[i][j] == 2) {//记录起点
stx = i;
sty = j;
}
}
}
dfs(0, stx, sty, 6);
if (min1 == 0x7ffff)//如果min1没变
cout << "-1";//那么没法到达
else cout << min1;
return 0;
}
下面贴上dalao们的思路和代码
第一位大佬利用状态图遍历的方法,因为数据量不大,所以通过一个四维数组来表示每一种不同的状态,细节如下:
本题解乃是用 dfs 做的时间复杂度最低的题解,时间复杂度: O(n^4)。
基础模板大家应该都会写,这里主讲优化。
优化
楼上的题解都是用的三维的数组来优化,而我是用的四维,具体请看:
定义 bool 数组 vis,四个维度分别为当前的行坐标,当前的列坐标,当前的血量,以及当前的步数。
为什么怎么定,因为不管在哪种情况下,这个 vis 数组的每个位置都是唯一的,一旦重复出现,后面就会做一样的情况,这点可以自己思考。
那么,它需要回溯吗?不需要,因为同一种情况只能出现一次,如果有重复出现,可以直接 return 掉,因为后面做过的操作,前面已经做过了。
下一步,第四个维度:步数。为什么说步数难,因为这道题题目说明可以走回头路,而以上题解都说的不够准确,因为步数不一定小于格子数量。但是,由于数据小,步数肯定在 100100 步以内,所以,第四个维度的大小要设为 105105 不然就会数组越界。
另外,个人建议把血量和步数当个参数,会更方便。
代码
理清思路,代码就出来了:
#include<bits/stdc++.h>
using namespace std;
bool vis[15][15][7][105]; //搞懂它是什么意思
int n, m, a[15][15], sx, sy, fx, fy, mini = 1e9;
int dx[10] = {0, 0, -1, 1};
int dy[10] = {-1, 1, 0, 0};
void dfs(int x, int y, int xue, int ans){ //个人建议传参
if(x < 1 || x > n || y < 1 || y > m || a[x][y] == 0 || xue == 0 || ans > 100 || vis[x][y][xue][ans]){ //如果当前这一步不行
return ;
}
vis[x][y][xue][ans] = true; //此次状态标记为true
if(a[x][y] == 4){ //回满血
xue = 6;
}
if(x == fx && y == fy){ //到达终点
mini = min(mini, ans);
return ;
}
for(int i = 0; i < 4; i++){ //4个方向
dfs(x + dx[i], y + dy[i], xue - 1, ans + 1);
}
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
cin >> a[i][j];
if(a[i][j] == 2){ //记录起点和终点
sx = i;
sy = j;
}
if(a[i][j] == 3){
fx = i;
fy = j;
}
}
}
dfs(sx, sy, 6, 0);
cout << (mini == 1e9 ? -1 : mini); //三目运算符,如果mini为1e9,输出-1,否则,输出mini
return 0;
}
最后提醒
-
用此方法可以 AC 此题,提交记录
-
此方法名为状态图遍历。
-
模板不会打的看上面几位大佬的代码
-
证明时间复杂度为: O(n^4)O(n4),可以证明,此处不过多解释.
第二个大佬用的是BFS解法
引言
显然,这是一道迷宫类的题目,而解决迷宫类题目最常用的算法是广度优先搜索(Breadth First Search,简称 BFS)。
如果你还不知道什么是 BFS,可以参考这道题目,以及我的题解。
当然,这道题也有使用深度优先搜索(Depth First Search,简称 DFS)的解法。一定程度上,DFS 解决迷宫问题会比 BFS 麻烦,如果你想尝试,可以参考其他人的题解,在此不再赘述。
细节
若小 H 在鼠标或家所在的格子上 HP 刚好降为 00,小 H 也会死去。如下面这组数据:
1 7
2 0 0 0 0 0 3
小 H 无法安全到家。
所以当小 H 的 PH 为 11 时,我们即可判定小 H 已经死亡,因为他无论下一步走到何种格子上都会直接死去。
解法
在传统的迷宫问题中,每个格子最多只能被搜索一次,所以可以使用一个 bool
型的 visited[]
数组来记录每个格子是否被访问过。
而在本题中,稍加思考就会发现,因为捡鼠标可以补充 HP,所以可能会出现最优解需要多次经过同一个格子的情况,例如下面这组数据:
6 6
2 0 0 0 0 0
1 0 0 0 0 0
1 0 0 0 0 0
1 1 4 0 0 0
1 0 0 0 0 0
1 4 1 1 1 3
作图如下:
令 (i,j)(i,j) 表示第 ii 行第 jj 列的格子,则图中:
- (1,1)(1,1) 的绿色格子表示小 H 的出发点;
- (6,6)(6,6) 的绿色格子表示小 H 的家;
- (4,3)(4,3) 和 (6,2)(6,2) 的蓝色格子表示鼠标;
- 其余的黑色格子表示障碍物,白色格子表示空地。
小 H 回家的路径如下图的红色箭头所示:
显然,在这组数据中,存在且仅存在这一种回家的路径,因为如果小 H 不捡 (4,3)(4,3) 的鼠标,他将在 (6,2)(6,2) 刚好死去。
即在某些情况中,若不重复经过一些格子,根本无法到达终点。
在这个路径中,(4,1)(4,1) 和 (4,2)(4,2) 都被经过了两次。
本题的突破点在于:在何种情况下,一个格子可以被重复经过?
答案是本次经过这个格子时剩余的 HP 都大于(不是大于或等于)之前经过时剩余的 HP 时。
因为多次经过一个格子时,步数必定比之前经过时大,所以若当前状态比之前的状态更优,则必定 HP 更高。因此上述结论成立。
仍用上面这组数据举例:
(4,1)(4,1) 在第一次被经过时,步数为 33,HP 为 33,而第二次被经过时步数为 77,HP 为 44;
(4,2)(4,2) 在第一次被经过时,步数为 44,HP 为 22,而第二次被经过时步数为 66,HP 为 55。
所以把传统 BFS 中的 visited[]
数组改为 int
类型,用于记录经过这个格子时最大的 HP 即可,
当尝试扩展一个空地时,若发现当前 HP 大于之前的最大 HP,即可成功扩展。
而有鼠标的格子最多只能被经过一次,因为每次经过这个格子时,HP 都会补满。
代码
完整 AC 代码如下:
其中 exit()
函数用于直接结束程序,函数参数为 00 时表示程序正常结束,可在输出最终答案后免去函数返回的麻烦,它包含在 <cstdlib>
头文件中。
#include <cstdio>
#include <cstdlib>
#include <queue>
using std::queue;
struct Place //用结构体存储经过一个格子时的状态:当前格子的横纵坐标、步数以及 HP
{
int x,y,step,HP;
};
int n,m;
int square[10][10]={}; //格子的种类
int visited[10][10]={}; //经过一个格子时的最大 HP
int dx[4]={1,-1,0,0},dy[4]={0,0,1,-1};
queue<Place> que; //广度优先搜索需要的队列
void BFS() //广度优先搜索
{
while(!que.empty())
{
int x=que.front().x,y=que.front().y,step=que.front().step,HP=que.front().HP;
que.pop();
if(square[x][y]==3)
{
printf("%d\n",step); //第一次扩展到家所在的格子,直接输出当前步数并结束程序
exit(0);
}
if(HP>1) //HP 小于或等于 1 则判定死亡
{
for(int i=0;i<=3;++i)
{
int nx=x+dx[i],ny=y+dy[i];
if(nx>=1 && nx<=n && ny>=1 && ny<=m) //确保尝试扩展的格子坐标在合法范围内
{
if(square[nx][ny]==1 || square[nx][ny]==3) //尝试扩展的格子是空地或小 H 的家
{
if(visited[nx][ny]<HP-1) //本次经过这个格子时的 HP 小于之前经过时的最大 HP
{
visited[nx][ny]=HP-1;
que.push(Place{nx,ny,step+1,HP-1}); //步数增加 1,HP 减少 1
}
}
if(square[nx][ny]==4) //尝试扩展的格子有鼠标
{
if(!visited[nx][ny]) //有鼠标的格子最多只能被经过一次
{
visited[nx][ny]=1;
que.push(Place{nx,ny,step+1,6}); //步数增加 1,HP 补满
}
}
}
}
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
scanf("%d",&square[i][j]);
if(square[i][j]==2)
{
que.push(Place{i,j,0,6}); //将出发点入队
}
}
}
BFS();
puts("-1"); //搜索结束后仍没有到家,判定无解
return 0;
}