胜利大逃亡 - 九度 OJ 1456

胜利大逃亡 - 九度 OJ 1456

题目

时间限制:2 秒 内存限制:32 兆 特殊判题:否
题目描述:
Ignatius 被魔王抓走了,有一天魔王出差去了,这可是 Ignatius 逃亡的好机会.魔王住在一个城堡里,城堡是一个 ABC 的立方体,可以被表示成 A 个 B*C 的矩阵,刚开始 Ignatius 被关在(0,0,0)的位置,离开城堡的门在(A-1,B-1,C-1)的位置,现在知道魔王将在 T 分钟后回到城堡,Ignatius 每分钟能从一个坐标走到相邻的六个坐标中的其中一个.现在给你城堡的地图,请你计算出 Ignatius 能否在魔王回来前离开城堡(只要走到出口就算离开城堡,如果走到出口的时候魔王刚好回来也算逃亡成功),如果可以请输出需要多少分钟才能离开,如果不能则出-1。
输入:
输入数据的第一行是一个正整数 K,表明测试数据的数量.每组测试数据的第一行是四个正整数 A,B,C 和 T(1<=A,B,C<=50,1<=T<=1000),它们分别代表城堡的大小和魔王回来的时间.然后是 A 块输入数据(先是第 0 块,然后是第 1 块,第 2块…),每块输入数据有 B 行,每行有 C 个正整数,代表迷宫的布局,其中 0 代表路,1代表墙。
输出:
对于每组测试数据,如果 Ignatius 能够在魔王回来前离开城堡,那么请输出他最少需要多少分钟,否则输出-1.
样例输入:
1
3 3 4 20
0 1 1 1
0 0 1 1
0 1 1 1
1 1 1 1
1 0 0 1
0 1 1 1
0 0 0 0
0 1 1 0
0 1 1 0
样例输出:
11

在本题中,有一个三维的迷宫,每个迷宫的点都用三维坐标(x,y,z)表示。主人公每次行走仅能走到上、下、左、右、前、后与主人公所在点相邻的位置,即从(x,y,z)点行走至(x-1,y,z)、(x+1,y,z)、(x,y+1,z)、(x,y-1,z)、(x,y,z+1)、(x,y,z-1)六个点的其中一个。在这其中还存在着一些墙,主人公在任何情况下都不能走到墙所在的位置上。求从点(0,0,0)走到点(A-1,B-1,C-1)最少需要几步。

分析此查找问题可得:
查找空间:该题的查找空间中的元素不再是之前题中的一个数或者几个数。它由所有从点(0,0,0)到点(A-1,B-1,C-1)合法的行走路径组成。
查找目标:在查找空间中的所有路径中寻找一条最短的路径,即行走步数最少的路径。
查找方法:与以往相比,在广度优先搜索中的查找方法将变的有些特殊,它不再机械地、暴力地遍历查找空间中所有路径,而是采用了某种策略。

在探讨这种特殊的查找方法之前,我们先指明该问题的状态。为了能够更一般的考虑搜索的问题,我们常在搜索问题中指明某种状态,从而使搜索问题变为对状态的搜索。在本问题中,由于要查找从起点到终点的最短耗时,设定状态(x,y,z,t)四元组,其中(x,y,z)为某个点的坐标,t 为从(0,0,0)点走到这个点所耗费的时间。在指明该问题的状态以后,查找的几个相关要素也相应的发生改变。

查找空间:也被称为搜索空间,从之前的所有路径变为对所有状态的搜索,即所有可能出现的四元组(x,y,z,t)。
查找目标:也被称为搜索目标,即在所有的状态中搜索这样一个四元组(x,y,z,t),其中 x、y、z 分别等于 A-1、B-1、C-1,t 为达到这个状态所需要的最少的时间。
查找方法:在确定状态的定义之前我们并没有讲明其查找方法,这是因为我们实际上并不是在原始查找空间中进行查找的,而是在我们人为定义的状态上进行所需的查找,所以其查找方法是针对状态来定义的。我们将通过状态的扩展转移来遍历查找所有的状态,下面给出其具体方法。

由于可以由任意一个点经过一秒的行走进入下一个点,所以由任意一个状态(x,y,z,t)可以扩展得到下面六个状态((x-1,y,z,t+1)、(x+1,y,z,t+1)、(x,y+1,z,t+1)、(x,y-1,z,t+1)、(x,y,z+1,t+1)、(x,y,z-1,t+1))中合法的所有状态,所谓合法即该点不是墙的所在点且该点在立方体范围之内。为了找到到达点(A-1、B-1、C-1)的最短时间,我们从初始状态(0,0,0,0)开始,按照状态的不断扩展转移查找每一个状态。将初始状态视为根节点,并将每一个状态扩展得到的新状态视为该状态的儿子结点,那么状态的转移与生成就呈现出了树的形态,如下图:
在这里插入图片描述
我们将这棵包含搜索空间中所有状态的树称为解答树,我们采用的搜索方法就是在对这棵解答树进行遍历时所使用的遍历方法。所谓广度优先搜索,即在遍历解答树时使每次状态转移时扩展出尽可能多的新状态,并且按照各个状态出现的先后顺序依次扩展它们。其在解答树上的表现为对解答树的层次遍历,先由根结点扩展出所有深度为 1 的结点,再由每一个深
度为 1 的结点扩展出所有深度为 2 的结点,依次类推,且先被扩展的结点其深度不大于后被扩展的结点深度。在这里,深度与所需的行走时间等价。这样,当搜索过程第一次查找到状态中坐标为终点的结点,其记录的时间即为所需最短时间。

但是,即便这样所需查找的状态还是非常得多,最坏情况下,因为每个结点都能扩展出六个新结点,那么仅走了 10 步,其状态数就会达到 6 的十次方,需要大量的时间才能依次遍历完这些状态。那么,我们必须采取相应的措施来制约状态的无限扩展。这个措施被称为剪枝。

剪枝,顾名思义即剪去解答树上不可能存在我们所需答案的子树,从而大大减少所需查找的状态总数。在本题中,我们可以注意到这样一个细节。在起点走向终点的最短路径上,到达任意一个中间结点所用的时间都是起点到达这个结点的最短时间。那么,在搜索过程中,若有状态(x,y,z,t),其中 t 不是从起点到达(x,y,z)的最短时间,那么所要查找的答案必不可能由该状态进行若干次扩展后得到。在解答树上,即所要查找的状态结点,不可能在该状态结点的子树上。有了这个结论,又考虑到广度优先搜索中,先查找到的状态深度必不大于后查找到的状态深度(深度与状态中耗时成正比),所以包含每个立方体中坐标的状态至多被扩展一次。例如,当我们第一次查找到包含点(x,y,z)的坐标状态后,其后查找到的任意包含该坐标的状态都不必被扩展,这是因为在后续被查找的状态中,所耗时间 t 必不小于先被查找到的状态。这样,我们限定了每个坐标仅有一个有效状态,所需遍历的状态总数大大降低,在本例中所需遍历的状态总数变为 ABC,完全在我们可以接受的范围内。

明确了查找的方法后,我们来讨论其实现。首先,使用如下结构体保存每一个状态:

struct N {
int x , y , z; //位置坐标
int t; //所需时间
}; 

其次,为了实现各个状态按照其被查找到的顺序依次转移扩展,我们需要使用一个队列。即将每次扩展得到的新状态放入队列中,待排在其之前的状态都被扩展完成后,该状态才能得到扩展。

最后,为了防止对无效状态的搜索,我们需要一个标记数组 mark[x][y][z],当已经得到过包含坐标(x,y,z)的状态后,即把 mark[x][y][z]置为 true,当下次再由某状态扩展出包含该坐标的状态时,则直接丢弃,不对其进行任何处理。

#include <stdio.h>
#include <queue>

using namespace std;

bool mark[50][50][50];//标记数组
int maze[50][50][50];//保存立方体信息

struct N{//状态结构体
    int x,y,z;
    int t;
};

queue<N> Q;//队列,队列中的元素为状态
int go[][3]={
    //坐标变换数组,由坐标(x,y,z)扩展得到的新坐标
    //均可通过(x+go[i][0],y+go[i][1],z+go[i][2])得到
    1,0,0,
    -1,0,0,
    0,1,0,
    0,-1,0,
    0,0,1,
    0,0,-1
};

int BFS(int a,int b,int c){
    //广度优先搜索,返回其最少耗时
    while(Q.empty()==false){
        //当队列中仍有元素可以扩展时循环
        N now=Q.front();//得到队头状态
        Q.pop();//从队列中弹出队头状态
        for(int i=0;i<6;i++){
            //依次扩展其六个相邻节点
            int nx=now.x+go[i][0];
            int ny=now.y+go[i][1];
            int nz=now.z+go[i][2];//计算新坐标
            if(nx<0 || nx>=a || ny<0 || ny>=b || nz<0 || nz>=c){
                continue;//若新坐标在立方体外,则丢弃该坐标
            }
            if(maze[nx][ny][nz]==1)continue;//若该位置为墙,则丢弃该坐标
            if(maze[nx][ny][nz]==true)continue;
            //若包含该坐标的状态已经被得到过,则丢弃该状态

            N tmp;//新的状态
            tmp.x=nx;
            tmp.y=ny;
            tmp.z=nz;//新状态的耗时
            tmp.t=now.t+1;//新状态的耗时
            Q.push(tmp);//将该状态放入队列
            mark[nx][ny][nz]=true;//标记该坐标
            if(nx==a-1 && ny==b-1 && nz==c-1){
                //若该坐标即为终点,可直接返回其耗时
                return tmp.t;
            }
        }
    }
    return -1;
    //若所有的状态被查找完后,仍得不到所需坐标,则返回-1
}

int main()
{
    int T;
    scanf("%d",&T);
    while(T--){
        int a,b,c,t;
        scanf("%d%d%d%d",&a,&b,&c,&t);//输入
        for(int i=0;i<a;i++){
            for(int j=0;j<b;j++){
                for(int k=0;k<c;k++){
                    scanf("%d",&maze[i][j][k]);
                    //输入立方体信息

                    mark[i][j][k]=false;//初始化标记数组
                }
            }
        }

        while(Q.empty()==false)Q.pop();//清空队列
        mark[0][0][0]=true;//标记起点
        N tmp;
        tmp.t=tmp.z=tmp.y=tmp.x=0;//初始状态
        Q.push(tmp);//将初始状态放入队列
        int rec=BFS(a,b,c);//广度优先搜索
        if(rec<=t)printf("%d\n",rec);//若所需时间符合条件,则输出
        else printf("-1\n");//否则输出-1
    }
    return 0;
}

我们通过状态之间的相互扩展完成在所有状态集合中的搜索,并查找我们需要的状态。利用这种手段,我们将原本对路径的搜索转化到了对状态的搜索上来。广度优先搜索即对由状态间的相互转移构成的解答树进行的按层次遍历。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值