每日一题|POJ3523 题解(双向BFS+状态压缩)

题面

The Morning after Halloween

Time Limit: 8000MS           Memory Limit: 65536K

Description

You are working for an amusement park as an operator of an obakeyashiki, or a haunted house, in which guests walk through narrow and dark corridors. The house is proud of their lively ghosts, which are actually robots remotely controlled by the operator, hiding here and there in the corridors. One morning, you found that the ghosts are not in the positions where they are supposed to be. Ah, yesterday was Halloween. Believe or not, paranormal spirits have moved them around the corridors in the night. You have to move them into their right positions before guests come. Your manager is eager to know how long it takes to restore the ghosts.

你在一家游乐园工作,经营着一家鬼屋,客人在里面穿过狭窄黑暗的走廊。这所房子以它们的鬼魂而自豪,这些鬼魂实际上是由操作员远程控制的机器人,躲在走廊里。一天早上,你发现鬼魂不在他们应该在的位置。啊,昨天是万圣节。信不信由你,超自然的魔力在夜里把他们带到走廊里。在客人到来之前,你必须将他们转移到正确的位置。你的经理很想知道恢复鬼魂需要多长时间。

In this problem, you are asked to write a program that, given a floor map of a house, finds the smallest number of steps to move all ghosts to the positions where they are supposed to be.

在这个问题中,你被要求编写一个程序,在给定房子的楼层图的情况下,求将所有鬼魂移动到它们应该所在的位置的最小步骤。

A floor consists of a matrix of square cells. A cell is either a wall cell where ghosts cannot move into or a corridor cell where they can.

地板是由正方形单元组成的矩阵。单元要么是鬼魂无法移动到的墙单元,要么是可以移动到的走廊单元。

At each step, you can move any number of ghosts simultaneously. Every ghost can either stay in the current cell, or move to one of the corridor cells in its 4-neighborhood (i.e. immediately left, right, up or down), if the ghosts satisfy the following conditions:

在每一步中,都可以同时移动任意数量的鬼魂。每个重影都可以停留在当前单元中,或者移动到其4邻域中的一个走廊单元(即,立即向左、向右、向上或向下),当鬼魂满足如下条件时

No more than one ghost occupies one position at the end of the step.

在步骤结束时,不超过一个鬼魂占据一个位置。

No pair of ghosts exchange their positions one another in the step.没有一对鬼魂在台阶上互相交换位置。

For example, suppose ghosts are located as shown in the following (partial) map, where a sharp sign (‘#’) represents a wall cell and ‘a’, ‘b’, and ‘c’ ghosts.

例如,假设鬼魂的位置如以下(局部)图所示,其中尖锐的符号(“#”)表示墙单元和“a”、“b”和“c”鬼魂

The input consists of at most 10 datasets, each of which represents a floor map of a house. The format of a dataset is as follows.

输入最多由10组数据成,每组数据代表一所房子的楼层图。格式如下。

w     h     n    

c11  c12  ⋯     c1w

c21  c22  ⋯     c2w

⋮     ⋮     ⋱     ⋮

ch1  ch2  ⋯     chw

w, h and n in the first line are integers, separated by a space. w and h are the floor width and height of the house, respectively. n is the number of ghosts. They satisfy the following constraints.

第一行中的w,h和n是整数,用空格分隔。w和h分别是房屋的楼层宽度和高度。n是鬼魂的数量。它们满足以下约束条件

4 ≤ w ≤ 16, 4 ≤ h ≤ 16, 1 ≤ n ≤ 3

Subsequent h lines of w characters are the floor map. Each of cij is either:

H行w列的数据包含

a ‘#’ representing a wall cell,

#表示墙

a lowercase letter representing a corridor cell which is the initial position of a ghost,

小写字母是鬼魂的初始位置

an uppercase letter representing a corridor cell which is the position where the ghost corresponding to its lowercase letter is supposed to be, or

大写字母是鬼魂的目标位置

a space representing a corridor cell that is none of the above.

空格代表空走廊

In each map, each of the first n letters from a and the first n letters from A appears once and only once. Outermost cells of a map are walls; i.e. all characters of the first and last lines are sharps; and the first and last characters on each line are also sharps. All corridor cells in a map are connected; i.e. given a corridor cell, you can reach any other corridor cell by following corridor cells in the 4-neighborhoods. Similarly, all wall cells are connected. Any 2 × 2 area on any map has at least one sharp. You can assume that every map has a sequence of moves of ghosts that restores all ghosts to the positions where they are supposed to be.

在每个楼层图中,a的前n个字母和a的前n个字母中的每一个都出现一次,而且只出现一次。地图最外面的单元格是墙;即第一行和最后一行的所有字符都是不可移动的;每行上的第一个和最后一个字符也是不可移动的。地图中的所有道路单元都已连接;即,给定一个走廊单元,您可以通过跟随4个邻域中的走廊单元到达任何其他走廊单元。类似地,所有壁细胞都是连接的。任何地图上的任何2×2区域都至少有一个不可以走通的地方。可以假设每个贴图都有一组鬼魂移动,将所有鬼魂恢复到它们应该所在的位置。

The last dataset is followed by a line containing three zeros separated by a space.

数据以0 0 0为结尾

Output

For each dataset in the input, one line containing the smallest number of steps to restore ghosts into the positions where they are supposed to be should be output. An output line should not contain extra characters such as spaces.

对于输入中的每个数据集,应该输出一行,该行包含将重影恢复到它们应该所在的位置的最小步骤数。输出行不应包含空格等额外字符。

Sample Input

5 5 2

#####

#A#B#

#   #

#b#a#

#####

16 4 3

################

## ########## ##

#    ABCcba    #

################

16 16 3

################

### ##    #   ##

##  #  ##   # c#

#  ## ########b#

# ##  # #   #  #

#  # ##   # # ##

##  a#  # # #  #

### ## #### ## #

##   #   #  #  #

#  ##### # ## ##

####   #B# #   #

##  C#   #   ###

#  # # ####### #

# ######  A##  #

#        #    ##

################

0 0 0

Sample Output

7

36

77

这题面是我自己翻译的,不通顺请见谅

思路

很明显了,求最小步数,用bfs

但是大家可以看到,这个题数据很凶猛,bfs如果不好好优化会TLE

那么这里我们可以使用 双向bfs

从起点和终点分别bfs,我们可以更快速的获得答案

Question1:三个鬼魂,我们如何表示状态?

简单,我们将所有鬼魂可以移动的位置(包含鬼魂所在的位置)找出来,对于点(a, b)我们给他一个序号c, 建立数组x,y,x[i]表示c的x坐标,y[i]是y坐标,建立id数组,对于id[a][b]表示这个点的序号c。

然后我们用三个序号分别表示出鬼魂(鬼魂位数量小于4)的位置,然后把这三个序号分别以二进制压缩存储在int变量中,就可以啦

#define convert(a, b, c) ((a<<16)|(b<<8)|c)

Question2: 有了状态表示方法,我们又怎么移动鬼魂,推导出新的状态?

根据第一问的状态,我们可以把我们压缩以后的状态转换回三个序号,分别是鬼魂所在的位置的序号(过会儿讨论鬼魂不足三个的情况)

这里我们建立mov数组,分别表示可以当鬼魂在点(a,b)时,移动的策略(即上下左右和不动)

mov[c][i]表示鬼魂处于c位置时第i种移动策略,存储的是第i种移动策略移动的下一个点的序号

特别地,我们用mov[c][0]表示移动c点的移动策略数量

接下来,我们由鬼魂所在的位置序号推导出鬼魂的下一状态,进行bfs

	cnt=0, flag=0, ans=0;
	for (int i=0; i='a' && a[i][j]<='z') start[a[i][j]-'a'] = cnt;
				else if (a[i][j]>='A' && a[i][j]<='Z') ends[a[i][j]-'A'] = cnt;
			}
		}
	for (int i=1; i<=cnt; ++i){//处理每个可以移动的位置的策略
		mov[i][0] = 0;
		for (int j=0; j<4; ++j){
			int nx=x[i]+v[j][0], ny=y[i]+v[j][1];
			if (nx<0 || nx>=h || ny<0 || ny>=w || a[nx][ny]=='#') continue ;
			mov[i][++mov[i][0]] = id[nx][ny];
		}//上下左右
		mov[i][++mov[i][0]] = i;//不动
	}
	if (n<=2){
	//如果不够三个鬼魂,我们就把鬼魂的位置赋予一个虚拟的位置补齐,移动策略只有一个——不动
		mov[++cnt][1]=cnt, mov[cnt][0]=1;
		ends[2]=cnt, start[2]=cnt;
	}
	if (n<=1){
		mov[++cnt][1]=cnt, mov[cnt][0]=1;
		ends[1]=cnt, start[1]=cnt;
	}

BFS

建立两个队列,q1,q2,分别存储从起点和终点开始bfs时的鬼魂状态

建立数组f1,f2,初始为-1,f[i][j][k]表示当第一个鬼魂在位置i,第二个鬼魂在j,第三个在k时移动的步数

当q1长度小于q2时,我们用q1bfs,反之,用q2

转换出鬼魂们的位置序号a,b,c,然后遍历移动策略,移动后的位置序号为a1,b1,c1

当f[a1][b1][c1]为-1时,f[a1][b1][c1] = f[a][b][c]+1,如果不是-1,说明已经走过了,continue

如果我们当前在对q1进行bfs,我们知道,当前的f1[a1][b1][c1]由f1[a][b][c]推导而来,如果我们发现f2[a1][b1][c1]不为空,就说明我们从终点开始bfs的时候已经来到出现了鬼魂分别位于a1,b1,c1的状态,我们得到答案为f1[a][b][c]+f2[a1][b1][c1]+1

细节是,我们每调用一次bfs只是将上一次拓展到的位置再拓展一遍,新拓展的位置先不管,选取元素少的那个队列进行bfs,也正是所谓的 双向 bfs

    memset(f1, -1, sizeof(f1));
	memset(f2, -1, sizeof(f2));
	f1[start[0]][start[1]][start[2]] = f2[ends[0]][ends[1]][ends[2]] = 0;
	s = convert(start[0], start[1], start[2]);
	e = convert(ends[0], ends[1], ends[2]);
	q1=queue(), q2=queue();
	q1.push(s), q2.push(e);//状态压缩及初始化
	while (!q1.empty() && !q2.empty()){
		if (q1.size()<=q2.size()){//元素少的先bfs一遍
			ans = bfs(q1, f1, f2);
		}
		else ans = bfs(q2, f2, f1);
		if (flag) return ;
	} 
int bfs(queue &q, int (&f1)[C][C][C], int (&f2)[C][C][C])
{ //q表示我们当前进行bfs的队列,f1为q对应的f数组,f2位另一个队列对应的f数组
	int t=q.size();
	while (t--){//取出上一轮bfs中拓展到的的所有状态
		int tmp=q.front(), a, b, c;
		q.pop();
		a=(tmp>>16)&255, b=(tmp>>8)&255, c=tmp&255;//转化
		//255用二进制表示为11111111,通过与运算,可以获得原序号
		for (int i=1; i<=mov[a][0]; ++i){//枚举移动策略
			int na=mov[a][i], nb, nc;
			for (int j=1; j<=mov[b][0]; ++j){
				nb = mov[b][j];
				if (check(na, nb, a, b)) continue ;
				for (int k=1; k<=mov[c][0]; ++k){
					nc = mov[c][k];
					if (check(na, nc, a, c) || check(nb, nc, b, c)) continue;
					//不发生冲突
					if (f1[na][nb][nc]!=-1) continue;
					if (f2[na][nb][nc]!=-1){//遇到了对面bfs过的位置
						flag = 1;
						return f1[a][b][c]+f2[na][nb][nc]+1;
					}
					f1[na][nb][nc] = f1[a][b][c]+1;
					q.push(convert(na, nb, nc)); //继续bfs
				}
			}
		}
	}
	return -1;
}
完整代码
#include
#include
#include
using namespace std;

#define convert(a, b, c) ((a<<16)|(b<<8)|c)
#define check(aa, bb, a, b) (aa==bb||(aa==b&&bb==a))
const int G = 4;
const int N = 20;
const int C = 14*14;
int w, h, n, cnt, ans, s, e;
int start[G], ends[G], v[4][2]={{-1,0},{0,1},{1,0},{0,-1}};
int f1[C][C][C], f2[C][C][C], mov[C][6], x[C], y[C], id[N][N];
char a[N][N];
bool flag;
queue q1, q2;

int bfs(queue &q, int (&f1)[C][C][C], int (&f2)[C][C][C])
{ //q表示我们当前进行bfs的队列,f1为q对应的f数组,f2位另一个队列对应的f数组
	int t=q.size();
	while (t--){//取出上一轮bfs中拓展到的的所有状态
		int tmp=q.front(), a, b, c;
		q.pop();
		a=(tmp>>16)&255, b=(tmp>>8)&255, c=tmp&255;//转化
		//255用二进制表示为11111111,通过与运算,可以获得原序号
		for (int i=1; i<=mov[a][0]; ++i){//枚举移动策略
			int na=mov[a][i], nb, nc;
			for (int j=1; j<=mov[b][0]; ++j){
				nb = mov[b][j];
				if (check(na, nb, a, b)) continue ;
				for (int k=1; k<=mov[c][0]; ++k){
					nc = mov[c][k];
					if (check(na, nc, a, c) || check(nb, nc, b, c)) continue;
					//不发生冲突
					if (f1[na][nb][nc]!=-1) continue;
					if (f2[na][nb][nc]!=-1){//遇到了对面bfs过的位置
						flag = 1;
						return f1[a][b][c]+f2[na][nb][nc]+1;
					}
					f1[na][nb][nc] = f1[a][b][c]+1;
					q.push(convert(na, nb, nc)); //继续bfs
				}
			}
		}
	}
	return -1;
}

void solve()
{
	
	cnt=0, flag=0, ans=0;
	for (int i=0; i='a' && a[i][j]<='z') start[a[i][j]-'a'] = cnt;
				else if (a[i][j]>='A' && a[i][j]<='Z') ends[a[i][j]-'A'] = cnt;
			}
		}
	for (int i=1; i<=cnt; ++i){//处理每个可以移动的位置的策略
		mov[i][0] = 0;
		for (int j=0; j<4; ++j){
			int nx=x[i]+v[j][0], ny=y[i]+v[j][1];
			if (nx<0 || nx>=h || ny<0 || ny>=w || a[nx][ny]=='#') continue ;
			mov[i][++mov[i][0]] = id[nx][ny];
		}//上下左右
		mov[i][++mov[i][0]] = i;//不动
	}
	if (n<=2){
	//如果不够三个鬼魂,我们就把鬼魂的位置赋予一个虚拟的位置补齐,移动策略只有一个——不动
		mov[++cnt][1]=cnt, mov[cnt][0]=1;
		ends[2]=cnt, start[2]=cnt;
	}
	if (n<=1){
		mov[++cnt][1]=cnt, mov[cnt][0]=1;
		ends[1]=cnt, start[1]=cnt;
	}
	memset(f1, -1, sizeof(f1));
	memset(f2, -1, sizeof(f2));
	f1[start[0]][start[1]][start[2]] = f2[ends[0]][ends[1]][ends[2]] = 0;
	s = convert(start[0], start[1], start[2]);
	e = convert(ends[0], ends[1], ends[2]);
	q1=queue(), q2=queue();
	q1.push(s), q2.push(e);//状态压缩及初始化
	while (!q1.empty() && !q2.empty()){
		if (q1.size()<=q2.size()){//元素少的先bfs一遍
			ans = bfs(q1, f1, f2);
		}
		else ans = bfs(q2, f2, f1);
		if (flag) return ;
	} 
}

int main()
{
	while (scanf("%d%d%d", &w, &h, &n)!=EOF && w && h && n){
		getchar();
		for (int i=0; i
补充

每日一题|POJ3523 题解(双向BFS+状态压缩)_第1张图片

 这一里面第一个是我,可以看一下我只用了2047ms,可见双向bfs的优越性

如果是单向bfs,不用状态压缩,不用优化,这个题大概率超时

你可能感兴趣的:(宽度优先,算法,c++,数据结构)