sicily1151 魔板解题报告
1. 原题中文大意
由8个大小相同方块组成的魔板,初始状态是1 2 3 4
8 7 65
对魔板可以进行三种操作,分别是A操作:上下行互换,B操作:每次以行循环右移一个), C操作(中间四小块顺时钟转一格)。题目有多组测试数据,每组数据输入第一行是最多容许步数,第二三行是目标状态,输出为操作的步数及操作序列。
2. 算法思想及主要用到的数据结构
这是一道典型的搜索题,给出一种初始状态,要求搜索到目标状态,可以用深度优先搜索或广度优先搜索来解决,即初始状态为搜索树的根节点,然后有ABC操作就有了三个孩子节点,每个孩子节点又有ABC操作,因此这是一颗完全三叉树,当扩展到目标状态时则这颗子树不用扩展下去了。如果是深度优先搜索的话,很可能要遍历整棵搜索树,考虑到问题的规模,用深搜不太妥当,肯定会超时。用广搜比较合适,但是如果只用单纯的广搜也很可能会超时,因为有很多状态会重复搜索,肯定会浪费很多时间,因此需要进行判重,当搜索要重复状态时,则不进行扩展。对于判重,可以用STL的set或者用康托展开,在搜索过程中,记录相关操作信息即可。
用到的数据结构:主要用到队列,数组。
3. 详细解题思路
可以存储每个状态对应的康托编码对应的操作序列来求解,即把所有状态先搜索完。
首先定义一个数据结构,储存当前状态(用一个int型整数来表示)和操作序列(用string表示),再定义这样的一个结构体数组,大小为40321(8个数组成的8位数最多只有8!=40320个), 然后再定义一个bool型访问数组,记录哪个下标已被访问。接下来进行广搜,根节点为(12348765, ""), 把根节点放进队列,此时队列的头指针为0,尾指针为1,然后只要头指针小于尾指针,则取出队列元素,然后这个元素的一个整数分别进行A、B、C操作(这些操作可以通过对整数运算来实现,可写3个操作函数),如果操作后的整数的康托编码是没有访问过的,则构造一个新节点,新节点的操作序列为之前的操作序列再加上A或B或C, 知道队列里没有未访问的节点为止。当遇到新节点时,储存这个节点编码对应的操作序列。
最后,当程序输入时,求出这个目标状态对应的康托编码,然后输出答案或-1.
4. 逐步求精算法描述
定义结构体:
structStatus
{
int num;
string ans;
Status(int s, string a)
{
num = s;
ans = a;
}
Status(){}
}q[40321];
q是结构体数组,最多只有40320种状态。
intfac[8] = { 1, 1, 2, 6, 24, 120, 720, 5040};
fac存阶乘大小,用于求康托编码;
boolvisit[40321];
visit[i]用于判断编码为i的状态是否已访问过;
stringans[40321];
ans[i]存编码为i的操作序列;
intA(int num) { // } // A操作
intB(int num) { // } // B操作
intC(int num) { //} // C操作
inthashCode(int num){ // return n; }
把状态为num进行康托展开,返回映射成的整数;
voidbfs() // 广搜
{ }//把所有节点状态对应的操作序列储存起来
5. 逐程序注释清单
#include<cstdio>
#include<cstring>
#include<string>
#include<set>
#include<queue>
#include<iostream>
#include<algorithm>
usingnamespace std;
intn, target;
inta[8];
boolvisit[40321];//visit[i]用于判断编码为i的状态是否已访问过;
stringans[40321];//ans[i]存编码为i的操作序列;
//fac存阶乘大小,用于求康托编码;
intfac[8] = { 1, 1, 2, 6, 24, 120, 720, 5040};
structStatus
{
int num;
string ans;
Status(int s, string a)
{
num = s;
ans = a;
}
Status(){}
}q[40321];//结构体数组,用于储存每种状态的数值和操作序列,最多有//40320种状态
intA(int num) // A操作,上下行交换
{
int t = 0;
t += num % 10000 * 10000;
t += num / 10000;
return t;
}
intB(int num) //B操作,每行循环右移一位
{
int t= 0;
t += num / 10000 % 10 * 10000000;
t += num / 100000 * 10000;
t += num % 10000 / 10;
t += num % 10 * 1000;
return t;
}
intC(int num) //C操作,中间四小块顺时钟转一格
{
int t = 0;
t += num / 10000000 * 10000000;
t += num / 1000 % 100 * 1000;
t += num % 10;
t += num / 100 % 10 * 1000000;
t += num % 100 / 10 * 100;
t += num / 100000 % 10 * 10;
t += num / 1000000 % 10 * 100000;
return t;
}
/* 把数值为num映射成一个整数,比如12345678对应0,因为由1-8组成的8
*位数中12345678最小,问题转化为求解有多少个整数比这个数小,这就是康
*托展开
*/
inthashCode(int num)
{
int n, cnt, i;
n = 0; //1-8组成的8位数中比num小的数的个数
i = 7;
int a[8];
while(num != 0)//把整数num各个位的数值存在a数组中
{
a[i--] = num % 10;
num /= 10;
}
for(i = 0; i < 8; ++i)
{
cnt = 0; //cnt记录后面的数有多少个数比这个小
for(int j = i + 1; j < 8; ++j)
{
if(a[i] > a[j])
cnt++;
}
n += fac[7 - i] * cnt; //判断到当前位,有多少个数比num小
}
return n;
}
/* 利用队列把整个搜索树无重复的搜索遍历,记录每个编码对应的操作序列, */
voidbfs() // 广搜
{
Status tem;
int t, code;
int head, tail;
memset(visit, false, sizeof(visit)); //首先设置所有的都未访问过
head = 0; //队列头指针
tail = 1; //队列尾指针
q[head] = Status(12348765, "");//根节点为(12348765,"")首先入队
visit[hashCode(q[head].num)] = true;
while(head < tail) //只要队列中还有未被访问过的元素
{
tem = q[head]; //取出队首元素
t = A(tem.num); //t为操作A之后对应的数值
code = hashCode(t);//code为t对应的康托编码
if(!visit[code]){ //如果编码为code的状态为访问过,则标记已访//问,把操作序列存入ans[code], 并把这个状态节点入队
visit[code] = true;
ans[code] = tem.ans +"A";
q[tail++] =Status(t,tem.ans+"A");
}
t = B(tem.num);//t为操作A之后对应的数值
code = hashCode(t);//code为t对应的康托编码
if(!visit[code]){//如果编码为code的状态为访问过,则标记已访
//问,把操作序列存入ans[code], 并把这个状态节点入队
visit[code] = true;
ans[code] = tem.ans +"B";
q[tail++] =Status(t,tem.ans+"B");
}
t = C(tem.num);//t为操作A之后对应的数值
code = hashCode(t);//code为t对应的康托编码
if(!visit[code]){//如果编码为code的状态为访问过,则标记已访
//问,把操作序列存入ans[code], 并把这个状态节点入队
visit[code] = true;
ans[code] = tem.ans +"C";
q[tail++] =Status(t,tem.ans+"C");
}
head++; //头指针向后移一位
}
}
intmain()
{
int i;
bfs();
while(scanf("%d", &n) != EOF&& n != -1)
{
for(i = 0; i < 8; ++i)
{
scanf("%d", a[i] + i);
}
int t = 1;
target = 0;
for(i = 7; i >= 0;--i)
{
target += t*a[i];
t *= 10;
}
int code = hashCode(target);
if(ans[code].size() > n)
cout << -1 << endl;
else
cout << ans[code].size()<< " " << ans[code] << endl;
}
return 0;
}
6. 测试数据
测试数据1: 1 1 2 3 4 8 7 6 5
输出: 0
测试数据2: 2 8 76 5 1 2 3 4
输出: 1 A
测试数据3: 3 1 3 6 4 8 2 7 5
输出: 3 ACA
测试数据4: 2 1 3 6 4 8 2 7 5
输出: -1
测试数据5: 8 4 3 2 6 5 1 8 7
输出: 5 ACBCA
7. 程序优化
在上面的程序中,我使用了一次宽度优先搜索来记录搜索树上的所有状态,用康托编码来判重,这算是比较快的,因为用时0.04s,输入我用了scanf,但是输出我用了cout,这是因为我用了C++的string类型,如果我用C语言的char类型来储存操作序列的话,输出时间应该会减少,总时间应该会减到0.03,但是空间可能会增大。