WELCOME

任何一个伟大的目标,都有一个微不足道的开始。

P4289 【一本通提高篇广搜的优化技巧】[HAOI2008]移动玩具

[HAOI2008]移动玩具

题目描述

在一个 4 × 4 4\times4 4×4 的方框内摆放了若干个相同的玩具,某人想将这些玩具重新摆放成为他心中理想的状态,规定移动时只能将玩具向上下左右四个方向移动,并且移动的位置不能有玩具,请你用最少的移动次数将初始的玩具状态移动到某人心中的目标状态。

输入格式

4 4 4 行表示玩具的初始状态,每行 4 4 4 个数字 1 \texttt{1} 1 0 \texttt{0} 0 1 \texttt{1} 1 表示方格中放置了玩具, 0 \texttt{0} 0 表示没有放置玩具。接着是一个空行。接下来 4 4 4 行表示玩具的目标状态,每行 4 4 4 个数字 1 \texttt{1} 1 0 \texttt{0} 0,意义同上。

输出格式

一个整数,所需要的最少移动次数。

输入输出样例

样例输入1

1111
0000
1110
0010 

1010
0101
1010
0101

样例输出1

4

讲解

变量

pre数组表示初始状态,dec1表示初始状态转化的十进制数。
tar数组表示目标状态,dec2表示目标状态转换的十进制数。
vis数组表示是否被访问过。
q队列用来存储玩具摆放的状态。
dx和dy数组是为了方便表示扩展而使用的。
有关玩具放置的状态的转化请移步至函数1。

函数1

int BintoDec(int a[6][6])
此函数的目的:
根据a数组,将玩具摆放的状态转化为十进制数。
变量解释:
a数组表示要转换的玩具摆放的状态。
具体思想:
由于每种玩具摆放的状态都是唯一的,
所以我们尝试着将其转化为一个数字。
大家可以发现,每种玩具摆放的状态其实是二进制的表示。
所以我们可以将其转化为十进制的数。
例如:样例中,初始状态可以转化为:1111000011100010。
所以可以将其转化为十进制的数。
可能你会考虑爆long long或爆空间的问题。
放心,玩具摆放的状态用十进制表示必不会超过2^16-1。
所以,我们可以将样例中的起始状态和目标状态
分别表示为61666和42405。
具体怎么转化呢?
我们先画一个玩具摆放的状态图:

列1列2列3列4
15141312
111098
7654
3210

可以发现,每个格子对应的乘方有如下的规律:
设格子(i,j)对应的乘方为G(i,j),
则有:G(i,j) = 16 - (i - 1) * 4 - j。
这个稍微想一下就可以证明出来,这里不再赘述。

函数2

void DectoBin(int x, int a[6][6])
此函数的目的:
根据x,将十进制状态转换回一开始二进制的状态。
变量解释:
x表示要转换回的十进制数,
a数组表示转换后的二进制状态。
具体思想:
呃。。。这就不用解释了吧。
这个大家应该很明白吧。
不明白就去看有关二进制的内容吧。

函数3

int judge(int x0, int y0, int xx, int yy)
此函数的目的:
判断是否越过边界且点所表示的数是否相同,是返回1,否表示0
变量解释:
xx,yy分别是点(x0,y0)经过扩展后得到的点的横坐标和纵坐标。
具体思想:\

函数4

void bfs()
此函数的目的:
通过bfs求得最少步数。
变量解释:
具体思想:
基本的bfs(STL<queue>)操作:
1. 将初始状态入队,并标记在队列中(vis[x]=1
2. 在队列不为空时循环执行以下操作:
(1) 取出队首,将其转化为原来的二进制状态,然后出队。
(2) 向上下左右扩展。
(3) 判断是否越过边界且两数相等。
if No then执行以下操作(最后需要还原a数组状态!):
1_ 提取扩展节点的状态,并将其转化为十进制数。
2_ 将扩展节点与原数交换
3_ 记录交换后的数组对应的十进制数
4_ 判断是否访问过
5_ 若没访问过,则标记已访问过,并记录交换后的数组对应的十进制数的父节点,即交换前的数组对应的十进制数。
6_ 否则直接跳过。
7_ 如果已经达到了目标状态,直接返回。
else then直接跳过

Step 1

输入&转化为二进制
由于输入形式中,数字都是连在一起的。
所以,大多数人都会想到用字符串或字符数组输入每一行,
然后将其转化为数字。但其实可以不用这么麻烦。
大家都知道,占x位输入是这么写的:
scanf("%xd", &a);(假定x是具体数字)
所以,对于一行连在一起的数字,我们可以占一位输入。
就是上面所写的那样。这样就可以使代码简洁明了。
然后再调用上面的BintoDec函数得到初始和目标状态的十进制数。

Step 2

bfs一波走着!
根据上面的函数,保证会有解。

Step 3

递进求答案、输出&程序结束

Code

#include <cstdio>
#include <cmath>
#include <algorithm>
#include <queue>
using namespace std;

int pre[6][6], tar[6][6], vis[65539], f[65539], dec1, dec2;
queue<int> q;
const int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1};
	/*
		变量:
		pre数组表示初始状态,dec1表示初始状态转化的十进制数。
		tar数组表示目标状态,dec2表示目标状态转换的十进制数。
		vis数组表示是否被访问过。 
		q队列用来存储玩具摆放的状态。 
		dx和dy数组是为了方便表示扩展而使用的。 
		有关玩具放置的状态的转化请移步至函数1。 
	*/
int BintoDec(int a[6][6]) { 
	/*	
		函数1:int BintoDec(int a[6][6])
		此函数的目的:
			根据a数组,将玩具摆放的状态转化为十进制数。 
		变量解释: 
			a数组表示要转换的玩具摆放的状态。 
		具体思想: 
			由于每种玩具摆放的状态都是唯一的, 
			所以我们尝试着将其转化为一个数字。
			大家可以发现,每种玩具摆放的状态其实是二进制的表示。
			所以我们可以将其转化为十进制的数。
			例如:样例中,初始状态可以转化为:1111000011100010。 
			所以可以将其转化为十进制的数。 
			可能你会考虑爆long long或爆空间的问题。
			放心,玩具摆放的状态用十进制表示必不会超过2^16-1。
			所以,我们可以将样例中的起始状态和目标状态
			分别表示为61666和42405。
			具体怎么转化呢?
			我们先画一个玩具摆放的状态图:	 
			  1     2     3     4
			1 15	14    13    12
			2 11    10    9     8
			3 7     6     5     4
			4 3     2     1     0
			可以发现,每个格子对应的乘方有如下的规律:
			设格子(i,j)对应的乘方为G(i,j),
			则有:G(i,j) = 16 - (i - 1) * 4 - j。
			这个稍微想一下就可以证明出来,这里不再赘述。 
	*/
	int res = 0;
	for(int i = 4; i >= 1; --i)
		for(int j = 4; j >= 1; --j)
			res += a[i][j] * pow(2, 16 - (i - 1) * 4 - j);
	return res;
}
	
void DectoBin(int x, int a[6][6]) {
	/*
		函数2:void DectoBin(int x, int a[6][6])
		此函数的目的:
			根据x,将十进制状态转换回一开始二进制的状态。
		变量解释: 
			x表示要转换回的十进制数,
			a数组表示转换后的二进制状态。 
		具体思想:
			呃。。。这就不用解释了吧。
			这个大家应该很明白吧。
			不明白就去看有关二进制的内容吧。 
	*/
	while(x) {
		for(int i = 4; i >= 1; --i)
			for(int j = 4; j >= 1; --j) {
				a[i][j] = x % 2;
				x /= 2;
			}
	} 
	//转换完毕 
}
int judge(int x0, int y0, int xx, int yy) {
	/*
		函数3:int judge(int x0, int y0, int xx, int yy)
		此函数的目的:
			判断是否越过边界且点所表示的数是否相同,是返回1,否表示0
		变量解释:
			xx,yy分别是点(x0,y0)经过扩展后得到的点的横坐标和纵坐标。
		具体思想:\ 
	*/
	return (xx >= 1) && (xx <= 4) && (yy >= 1) && (yy <= 4) && (pre[x0][y0] != pre[xx][yy]);
}
void bfs() {
	/*
		函数4:void bfs()
		此函数的目的:
			通过bfs求得最少步数。 
		变量解释:\
		具体思想:
			基本的bfs(STL<queue>)操作:
			1. 将初始状态入队,并标记在队列中(vis[x]=1)
			2. 在队列不为空时循环执行以下操作:
				(1)取出队首,将其转化为原来的二进制状态,然后出队。
				(2)向上下左右扩展。 
				(3)判断是否越过边界且两数相等。 
				if No then执行以下操作(最后需要还原a数组状态!):
					1_ 提取扩展节点的状态,并将其转化为十进制数。 
					2_ 将扩展节点与原数交换
					3_ 记录交换后的数组对应的十进制数
					4_ 判断是否访问过
					5_ 若没访问过,则标记已访问过,并记录交换后的数组对应的十进制数的父节点,即交换前的数组对应的十进制数。 
					6_ 否则直接跳过。 
					7_ 如果已经达到了目标状态,直接返回。
				else then直接跳过 
	*/
	q.push(dec1);
	vis[dec1] = 1;/*注意!一定要加!!!*/ 
	while(q.size()/*或者可以写!q.empty()*/) {
		int now = q.front();
		DectoBin(now, pre);
		q.pop();
		for(int i = 1; i <= 4; ++i)
			for(int j = 1; j <= 4; ++j) {
				int x0 = i, y0 = j;
				for(int k = 0; k < 4; ++k) {
					int xx = x0 + dx[k], yy = y0 + dy[k], flag = 0;
					if(judge(x0, y0, xx, yy)) {
						flag = 1;
						int tmpdec1 = BintoDec(pre);
						swap(pre[x0][y0], pre[xx][yy]);
						int tmpdec2 = BintoDec(pre);
						if(!vis[tmpdec2]) {
							vis[tmpdec2] = 1;
							f[tmpdec2] = tmpdec1;
							q.push(tmpdec2); 
						}
						if(tmpdec2 == dec2)	return;
					}
					if(flag)
						swap(pre[x0][y0], pre[xx][yy]);
				}
			} 
	}
}
int main() {
	/*
		Step 1:输入&转化为二进制 
		由于输入形式中,数字都是连在一起的。 
		所以,大多数人都会想到用字符串或字符数组输入每一行, 
		然后将其转化为数字。但其实可以不用这么麻烦。
		大家都知道,占x位输入是这么写的:
		scanf("%xd", &a);(假定x是具体数字)
		所以,对于一行连在一起的数字,我们可以占一位输入。
		就是上面所写的那样。这样就可以使代码简洁明了。
		然后再调用上面的BintoDec函数得到初始和目标状态的十进制数。 
	*/ 
	for(int i = 1; i <= 4; ++i)
		for(int j = 1; j <= 4; ++j)
			scanf("%1d", &pre[i][j]);
	for(int i = 1; i <= 4; ++i)
		for(int j = 1; j <= 4; ++j)
			scanf("%1d", &tar[i][j]);
	dec1 = BintoDec(pre), dec2 = BintoDec(tar);
	/*
		Step 2:bfs一波走着!
		根据上面的函数,保证会有解。 
	*/ 
	bfs();
	f[dec1] = 0;
	int ans = 0;
	/*
		Step 3:递进求答案、输出&程序结束 
	*/
	while(f[dec2])	ans++, dec2 = f[dec2];
	printf("%d", ans);
	return 0; 
} 

posted @ 2022-07-12 20:35  绿树公司  阅读(74)  评论(0)    收藏  举报