1. 题目详情
描述
佐助被大蛇丸诱骗走了,鸣人在多少时间内能追上他呢?
已知一张地图(以二维矩阵的形式表示)以及佐助和鸣人的位置。地图上的每个位置都可以走到,只不过有些位置上有大蛇丸的手下,需要先打败大蛇丸的手下才能到这些位置。鸣人有一定数量的查克拉,每一个单位的查克拉可以打败一个大蛇丸的手下。假设鸣人可以往上下左右四个方向移动,每移动一个距离需要花费1个单位时间,打败大蛇丸的手下不需要时间。如果鸣人查克拉消耗完了,则只可以走到没有大蛇丸手下的位置,不可以再移动到有大蛇丸手下的位置。佐助在此期间不移动,大蛇丸的手下也不移动。请问,鸣人要追上佐助最少需要花费多少时间?
输入
输入的第一行包含三个整数:M,N,T。代表M行N列的地图和鸣人初始的查克拉数量T。0 < M,N < 200,0 ≤ T < 10
后面是M行N列的地图,其中@代表鸣人,+代表佐助。*代表通路,#代表大蛇丸的手下。
输出
输出包含一个整数R,代表鸣人追上佐助最少需要花费的时间。如果鸣人无法追上佐助,则输出-1。
样例输入
样例输入1
4 4 1
#@##
**##
###+
****
样例输入2
4 4 2
#@##
**##
###+
****
样例输出
样例输出1
6
样例输出2
4
原题链接.
2.思路分析
2.1 整体思路
根据本题描述与输入,题目是一道有条件限制的迷宫最短路径搜索问题。鸣人的行走步长始终为1,且迷宫中节点间距离全部相等;如果没有查克拉的限制完全可以使用BFS算法求解(甚至直接横纵坐标作差求和)。
2.2 限制条件处理
根据BFS的性值,先扩展出的节点必然距离更短->花费时间更短;但由于本题的查克拉限制,到达同一节点花费时间更短的路径未必能成功营救佐助(可能经历了很多大蛇丸手下,后面有大蛇丸手下的路径就无法通过了)。鸣人的状态可以用一个四元组表示,即(x坐标,y坐标,查克拉量,当前时间);这里利用结构体实现。如果在查克拉限制的前提下最终能扩展出佐助所在节点则问题有解且第一次扩展到佐助的情况即为最优解。
2.3 剪枝
利用2.2中给出的方法能通过此题,鸣人多条路径的不同状态:(x,y,查克拉,当前时间)都会被扩展到,时空复杂度较高;我们对其进一步分析提前剪掉一些无用的分枝。在BFS中,同一节点如果在后面扩展出的节点如果查克拉量如果比之前扩展出节点的查克拉量更高则有必要扩展;反之则没有必要扩展。因此,我们可以为迷宫矩阵元素多添加一个参数即当前节点最先被扩展时的查克拉量。
3.注意点
- 由于查克拉限制的影响,本题中迷宫的每个格点其实可以经历2次或以上;不能强行套用每个节点只遍历一次的代码模板。
- 个人感觉本题难点在于鸣人状态的表示以及剪枝,代码实现难度不大。
- 本题中为了练习自行实现数据结构,使用了自定义的队列;其实完全可以使用STL。
4.AC代码
分析思路与注意点后,贴出思路对应的AC代码。
#include<iostream>
using namespace std;
int M,N,T;
int a;
struct node
{
char c;
int energy;
};
struct point
{
int x;
int y;
int time;
int t;
int parent;
};
int x_start,y_start,x_end,y_end;
node maze[201][201];
class Queue
{
public:
int rear;
int front;
point item[40001];
Queue()
{
rear = 0;
front = 0;
}
bool empty()
{
return rear == front;
}
void enqueue(int x,int y,int time,int t,int parent=-1)
{
item[rear].x = x;
item[rear].y = y;
item[rear].time = time;
item[rear].t = t;
item[rear].parent = parent;
++rear;
}
void enqueue(point p)
{
item[rear++] = p;
}
point dequeue()
{
++front;
return item[front-1];
}
int front_time()
{
return item[front].time;
}
};
void init()
{
for(int i=1;i<=M;++i)
{
for(int j=1;j<=N;++j)
{
//迷宫赋初值时,查克拉记为-1;当查克拉为0时,可以走平地,但不能同大蛇丸手下交战
cin>>maze[i][j].c;
maze[i][j].energy = -1;
if(maze[i][j].c == '@')
{
x_start = i;
y_start = j;
}
else if(maze[i][j].c == '+')
{
x_end = i;
y_end = j;
}
}
}
for(int i=0;i<=M+1;++i)
{
maze[i][0].c = '#';
maze[i][N+1].c = '#';
maze[i][0].energy =40;
maze[i][N+1].energy =40;
}
for(int j=0;j<=N+1;++j)
{
maze[0][j].c = '#';
maze[M+1][j].c = '#';
maze[0][j].energy = 40;
maze[M+1][j].energy = 40;
}
}
void out_put()
{
for(int i=0;i<=M+1;++i)
{
for(int j=0;j<=N+1;++j)
{
cout<<maze[i][j].c;
}
cout<<endl;
}
}
Queue q;
int maze_Naruto()
{
//队列某条路径中对某个节点进行扩展时,检测当前查克拉(消耗后)是否多于该点过去记录的查克拉;并将扩展的迷宫节点查克拉更新为当前查克拉。
//因为欲要扩展的节点可能过去被扩展过(之前的路径扩展该点的时间必然>=此次扩展),因此只有当前查克拉(也许经过消耗)大于原记录时才有扩展的必要。
point p;
q.enqueue(x_start,y_start,0,T);
while(!q.empty())
{
p = q.dequeue();
if((p.x==x_end)&&(p.y==y_end))
{
return p.time;
}
if(maze[p.x - 1][p.y].c == '#')
{
if(p.t - 1 > maze[p.x - 1][p.y].energy)
{
q.enqueue(p.x-1,p.y,p.time+1,p.t-1);
maze[p.x - 1][p.y].energy = p.t - 1;//更新当前查克拉
}
}
else
{
if(p.t > maze[p.x - 1][p.y].energy)
{
q.enqueue(p.x-1,p.y,p.time+1,p.t);
maze[p.x - 1][p.y].energy = p.t;
}
}
if(maze[p.x + 1][p.y].c == '#')
{
if(p.t - 1 > maze[p.x + 1][p.y].energy)
{
q.enqueue(p.x+1,p.y,p.time+1,p.t-1);
maze[p.x + 1][p.y].energy = p.t - 1;
}
}
else
{
if(p.t > maze[p.x + 1][p.y].energy)
{
q.enqueue(p.x+1,p.y,p.time+1,p.t);
maze[p.x + 1][p.y].energy = p.t;
}
}
if(maze[p.x][p.y - 1].c == '#')
{
if(p.t - 1 > maze[p.x][p.y - 1].energy)
{
q.enqueue(p.x,p.y - 1,p.time+1,p.t-1);
maze[p.x][p.y - 1].energy = p.t - 1;
}
}
else
{
if(p.t > maze[p.x][p.y - 1].energy)
{
q.enqueue(p.x,p.y-1,p.time+1,p.t);
maze[p.x][p.y - 1].energy = p.t;
}
}
if(maze[p.x][p.y + 1].c == '#')
{
if(p.t - 1 > maze[p.x][p.y + 1].energy)
{
q.enqueue(p.x,p.y + 1,p.time+1,p.t-1);
maze[p.x][p.y + 1].energy = p.t - 1;
}
}
else
{
if(p.t > maze[p.x][p.y + 1].energy)
{
q.enqueue(p.x,p.y+1,p.time+1,p.t);
maze[p.x][p.y + 1].energy = p.t;
}
}
}
return -1;
}
int main()
{
cin>>M>>N>>T;
init();
cout<<maze_Naruto();
}
5.总结
通过本题的练习对于BFS的理解有所深化,一味地套用解题模板遇到这题是真的会碰钉子。“状态空间”的设计也是需要我们重点考虑的,为什么像平时那样开一个visit数组(判重数组)每个节点遍历一次就OK的套路不适用了;因为本题有查克拉的限制,走到同一个节点但是查克拉不同其实是算作两个状态;从状态空间遍历这一角度思考,本题只是visit数组变复杂了;常规问题是(x坐标,y坐标)本题是(x坐标,y坐标,查克拉)本质上还是一样的。最后,由于状态空间变得复杂了,对其遍历过程进行化简就是非常重要的;剪枝实际上就是去掉了一些无用的状态遍历。
使用了比较naive的写法,代码有些冗余;后期可能会结合胡凡老师算法笔记中的一些模板写法进行优化。不过运行效率我还是比较满意的,百练上面是20ms。