吴昊品游戏核心算法 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),代表房间的大小,然后是一个M行N列的矩阵,代表房间的布局,其中0代表空的地板,1代表墙,2代表箱子的起始位置,3代表箱子要被推去的位置,4代表搬运工的起始位置(这种做法在经典游戏中的地图编辑器中是常见的,比如坦克大战的地图中,不同的道具也是用一个不同整型变量的二维数组来表征的)。
Output:对于每组测试数据,输出搬运工最少需要推动箱子多少格才能帮箱子推到指定位置,如果不能推到指定位置则输出-1。
Solve:
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