洛谷 P2802 回家 C++ DFS 深搜 BFS 广搜

题目描述

小 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;
}

最后提醒

  1. 用此方法可以 AC 此题,提交记录

  2. 此方法名为状态图遍历

  3. 模板不会打的看上面几位大佬的代码

  4. 证明时间复杂度为: O(n^4)O(n4),可以证明,此处不过多解释.

第二个大佬用的是BFS解法

题目链接:P2802 回家

引言

显然,这是一道迷宫类的题目,而解决迷宫类题目最常用的算法是广度优先搜索(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;
}
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Prudento

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值