吴昊品游戏核心算法 Round 14 —— 吴昊教你推箱子(priority_queue容器+BFS)(HDOJ 1254)

在吴昊品游戏核心算法Round 1中,我有介绍一个“企鹅推箱子”的游戏,我们来回顾一下一个经典游戏是如何经过包装,最后成为一个崭新的游戏的。首先是美工,通过3D的画面,给人一种 震撼力,在UI方面,考虑到智能手机的触屏特性,制造出很多的“动态效果”。在算法方面,通过搜索算法的引入,给游戏制造了一种附加功能,就是,你可以通 过搜索让自己告诉你应该如何进行下一步的方向控制(这一点有点像连连看中的放大镜提示)。在这样的经典游戏中,往往就这么一个小插件就会给整个游戏增色不 少。

 

  关于推箱子问题的解法的时间复杂度,最终被证明是NP难的,而且后来又被证明是PSPACE完全的,其具体的证明步骤我会在后面的文章中给出(准确说应该是转载吧,那位大神的名字是杨超老师)。

 推箱子简介:

 推箱子》(倉庫番)是一款经典电子游戏1982日本Thinking Rabbit公司首次发行。之后其他游戏开发者争相制作仿制或衍生作品。致使推箱子成为此类游戏的代名词。游戏要求玩家在二维地图上把箱子推到指定地点,当中牵涉到大量的空间逻辑推理。

 推箱子的规则:

  第一个《推箱子》的游戏规则,则是扮演工人的玩家,以“推”的方式,推动箱子。可以在没有阻碍物(如墙壁等的阻碍物)的情况下,向的地方移动。将箱子移动到指定点,达到指定数量,即可过关。

 但玩家移动箱子,有下列条件要注意:

  • 推到墙壁的箱子,玩家就不可以背对墙壁,把箱子推回到空处。即箱子只能以被推的方式被移动,不是以被拉的方式被移动。但如果玩家推至墙壁后,垂直墙壁的两侧没有阻碍物,则玩家可以朝这两个不同的方向推移箱子。
  • 一旦箱子被移动到角落,玩家没有任何方法再移动这个被推到角落的箱子。
  • 玩家不可同时推动两个或以上的箱子。假设工人面前有一个箱子,箱子的正前方又有一个箱子,则这两个箱子是不能被推动的(这个假设可以暂时不予以理睬,我以后会说明推箱子的各种变体,会提出推三个箱子游戏的玩法)。

 

 各种推箱子各种变体:

  (A)增加箱子的数目。

  (B)添加数量有限的炸弹破坏墙壁方可达成目标。

  (C)将重力感应添加到游戏中。

  (D)具有可以改变特性的墙壁、机关;可以收集的物品(原本是障碍物)。

 

 简化问题,抽象出模型(Source:HDOJ 1254):

 在一个M*N的房间里有一个箱子和一个搬运工,搬运工的工作就是把箱子推到指定的位置,注意,搬运工只能推箱子而不能拉箱子,因此如果箱子被推到一个角上(如图)那么箱子就不能再被移动了,如果箱子被推到一面墙上,那么箱子只能沿着墙移动。

 现在给定房间的结构,箱子的位置,搬运工的位置和箱子要被推去的位置,请你计算出搬运工至少要推动箱子多少格。

 

  Input:输入数据的第一行是一个整数T(1<=T<=20),代表测试数据的数量.然后是T组测试数据,每组测试数据的第一行是两个正整数M,N(2<=M,N<=7),代表房间的大小,然后是一个MN列的矩阵,代表房间的布局,其中0代表空的地板,1代表墙,2代表箱子的起始位置,3代表箱子要被推去的位置,4代表搬运工的起始位置(这种做法在经典游戏中的地图编辑器中是常见的,比如坦克大战的地图中,不同的道具也是用一个不同整型变量的二维数组来表征的)

  Output:对于每组测试数据,输出搬运工最少需要推动箱子多少格才能帮箱子推到指定位置,如果不能推到指定位置则输出-1

  Solve:

 

  1 /*
  2    Highlights:
  3               (1)定义四维变量flag[N][N][N][N]来标记每一个状态是否被访问过
  4               (2)用一个二维数组来表证地图,并且对不同的道具用整型数组标识
  5               (3)用每一个Node标识一个状态,并且用运算符重载的方式选择一个最少的步数
  6               (4)由于这里存在人和箱子两个实体,所以每次对各个方向进行搜索的时候,要分人是否可以挪动箱子进行分类讨论 
  7  */
  8  
  9  #include<iostream>
 10  //这里用的是STL中的优先队列容器
 11  #include<queue>
 12  #define N 8
 13  using namespace std;
 14  
 15  //定义一个游戏地图,以及地图的尺寸(n,m),和在搜索时需要用到的标记数组(这里是四维的10^4)
 16  int arr[N][N];
 17  //设置flag四位数组,来对每一个状态是否访问进行标记,如果该状态已经存在,则设置为true
 18  bool flag[N][N][N][N];
 19  int n,m;
 20  
 21  //每一个Node标识一个状态
 22  struct Node
 23  {
 24    int x;
 25    int y;
 26    int bx;
 27    int by;
 28    int t;
 29    //<运算符重载,这里对每一个结点的步数进行排序,friend友元是为了让结点类外面的对象可以访问这个类的成员
 30    friend bool operator < (Node a,Node b)
 31    {
 32      return a.t>b.t;       
 33    }       
 34  };
 35  
 36  int sx,sy,bx,by,ex,ey;
 37  //这里也可以定义两个一维数组来组成一个二维数组
 38  int dx[]={1,-1,0,0};
 39  int dy[]={0,0,-1,1};
 40  
 41  void BFS()
 42  {
 43    int i,j,k;
 44    int a,b,c;
 45    //设置一个装载结点的优先队列Q
 46    priority_queue<Node>Q;
 47    Node curn,nxtn;
 48    //初始化标志数组
 49    memset(flag,false,sizeof(flag));
 50    //这里标识搬运工是可以到达箱子的初始位置的
 51    flag[sx][sy][bx][by]=true;
 52    //标识人的起始位置和箱子的起始位置,并将这个状态下的步数设置为0(初始步数)
 53    curn.bx=bx;
 54    curn.by=by;
 55    curn.t=0;
 56    curn.x=sx;
 57    curn.y=sy;
 58    //装载第一个结点
 59    Q.push(curn);
 60    while(!Q.empty())
 61    {
 62      curn=Q.top();
 63      Q.pop();
 64      //当到达了终点时,将此时的步数记录如下
 65      if(curn.bx==ex&&curn.by==ey)
 66      {
 67        printf("%d\n",curn.t);
 68        return;                            
 69      }
 70      //根据广搜的原理,从四个不同的方向进行尝试        
 71      for(i=0;i<4;i++)
 72      {
 73        nxtn.x=curn.x+dx[i];
 74        nxtn.y=curn.y+dy[i];
 75        //这里先不慌着加步数,要经过一定的判断之后再加入
 76        nxtn.t=curn.t;
 77        //这里,如果下一步移动到的位置(1)在游戏地图范围内(2)没有墙壁的阻挡
 78        if(nxtn.x>=0&&nxtn.x<n&&nxtn.y>=0&&nxtn.y<m&&arr[nxtn.x][nxtn.y]!=1)
 79        {
 80          //加入箱子的位置就在人所移动方向的正前方(这里的方位以人所移动的方向为准)
 81          if(curn.bx==nxtn.x&&curn.by==nxtn.y)
 82          {
 83            a=nxtn.x+dx[i];
 84            b=nxtn.y+dy[i];
 85            //同上,这里再判断一下人的下一步所造成的情况,顺便,判断是否已经经历过,如果没有,则箱子移动,且步数++,设置flag,并进队列
 86            if(a>=0&&b>=0&&a<n&&b<m&&arr[a][b]!=1&&!flag[nxtn.x][nxtn.y][a][b])
 87            {
 88              nxtn.t++;
 89              nxtn.bx=a;
 90              nxtn.by=b;
 91              flag[nxtn.x][nxtn.y][nxtn.bx][nxtn.by]=true;
 92              Q.push(nxtn);                                                                   
 93            }                                    
 94          }                
 95          //如果不行的话,就当做这次的箱子没有动,将箱子的位置设为不变(人的位置还是变化了),考虑flag,如果没有经历过的话,标记一下状态
 96          else
 97          {
 98            nxtn.bx=curn.bx;
 99            nxtn.by=curn.by;
100            if(!flag[nxtn.x][nxtn.y][nxtn.bx][nxtn.by])
101            {
102              flag[nxtn.x][nxtn.y][nxtn.bx][nxtn.by]=true;
103              Q.push(nxtn);                                           
104            }    
105          }                                                  
106        }                
107      }         
108    }     
109    //实在是无解,则输出-1
110    printf("-1\n");
111  }
112  
113  int main()
114  {
115    int Case;
116    int i,j,k;
117    //读入案例数
118    scanf("%d",&Case);
119    while(Case--)
120    {
121      //读入地图的尺寸
122      scanf("%d%d",&n,&m);
123      //填充地图的每一个元素
124      for(i=0;i<n;i++)
125        for(j=0;j<m;j++)
126        {
127          scanf("%d",&arr[i][j]);                
128        }             
129      //设置初始值
130      for(i=0;i<n;i++)
131        for(j=0;j<m;j++)
132        {
133          //设置箱子的起始点,终止点和搬运工的起始位置,同时注意,将arr[i][j]重新置0,表示又回归为地面
134          if(arr[i][j]==2)
135          {
136            bx=i;
137            by=j;
138            arr[i][j]=0;                
139          }             
140          else if(arr[i][j]==3)
141          {
142            ex=i;
143            ey=j;
144            arr[i][j]=0;     
145          }   
146          else if(arr[i][j]==4)
147          {
148            sx=i;
149            sy=j;
150            arr[i][j]=0;     
151          }
152        }
153      BFS();
154    }
155    return 0;    
156  }
157 
158 

 

posted on 2013-02-28 15:00  吴昊系列  阅读(878)  评论(0编辑  收藏  举报

导航