您的位置:首页 > 其它

Sicily 1151. 魔板

2015-10-07 19:24 387 查看
题目链接在此

经典的状态转换搜索题。

一.原题中文大意

(1)描述

魔板由8个大小相同方块组成,分别用涂上不同颜色,用1到8的数字表示。其初始状态是:

1 2 3 4

8 7 6 5

对魔板可进行三种基本操作:

① A操作(上下行互换):

8 7 6 5

1 2 3 4

② B操作(每次以行循环右移一个):

4 1 2 3

5 8 7 6

③ C操作(中间四小块顺时针转一格):

1 7 2 4

8 6 3 5

用上述三种基本操作,可将任一种状态装换成另一种状态。

(2)输入

输入包括多个要求解的魔板,每个魔板用三行描述。

第一行步数N(可能超过10的整数),表示最多容许的步数。

第二、第三行表示目标状态,按照魔板的形状,颜色用1到8的表示。

当N等于-1的时候,表示输入结束。

(3)输出

对于每一个要求解的魔板,输出一行。

首先是一个整数M,表示你找到解答所需要的步数。接着若干个空格之后,从第一步开始按顺序给出M步操作(每一步是A、B或C),相邻两个操作之间没有任何空格。

注意:如果不能达到,则M输出-1即可。

(4)输入样例

4 5 8 7 6 4 1 2 3

3 8 7 6 5 1 2 3 4

-1

(5)输出样例

2 AB

1 A

二.数据结构与算法思想

(1) 数据结构

①用字符串记录每个魔板的状态,如初始状态记作"12348765";

②用结构体记录每个魔板的状态和它从起始状态到该状态的路径:

struct boardTree {
string board;
string path;
};


③广度优先搜索要用到队列,本解法使用STL中的queue:

queue<boardTree>


④用数组存储是否访问过某状态:

由于最多有8!=40320个状态,设置数组 bool visit[40320]来记录。

(2)算法思想

①使用广度优先搜索算法搜索状态;

②用康拓展开压缩状态,将8!=40320个状态所对应的40320种全排列顺序一一映射成常数,作为visit数组的下标。

③剪枝:由于任何一状态经过AA,或BBBB,或CCCC操作后会还原该状态,所以对于路径中出现“AA”,或“BBBB”,或“CCCC”子串的状态,不予进行判断或将其子状态放入队列。

三.详细解题思路

(1)广度优先搜索算法

①先将初始状态魔板放入队列。

②当队列不为空时,

a.检查队首魔板的路径长度是否超出限制,若超出,跳过b,进入c;

b.检查队首魔板的状态是否与终态一致,若一致则输出结果。否则进入c。

c.将该状态分别经过A、B、C操作所的状态依次放入队列。队首出队,重复步骤②。

(2)康托展开

康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。康托展开的实质是计算当前排列在所有由小到大全排列中的顺序。其计算公式如下:

把一个整数X展开成如下形式:

X=a
*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[2]*1!+a[1]*0![1]

其中,a为整数,并且0<=a[i]<i(1<=i<=n)

(3)A、B、C操作的实现

主要思想是将“旧”魔板的状态中每一位的值按规律映射到“新”魔板的状态中每一位的值:

string operationA(string board) {
string tmp = "";
int index[8] = { 4, 5, 6, 7, 0, 1, 2, 3 };
for (int i = 0; i < 8; i++)
tmp += board[index[i]];
return tmp;
}


string operationB(string board) {
string tmp = "";
int index[8] = { 3, 0, 1, 2, 7, 4, 5, 6 };
for (int i = 0; i < 8; i++)
tmp += board[index[i]];
return tmp;
}


string operationC(string board) {
string tmp = "";
int index[8] = { 0, 5, 1, 3, 4, 6, 2, 7 };
for (int i = 0; i < 8; i++)
tmp += board[index[i]];
return tmp;
}


四.逐步求精算法描述

本题解最重要的部分为搜索算法:

①主要是利用construct这一函数实现:传入的是目标状态(string target),路径深度限制(int limit),标记是否成功找到目标的布尔值(bool& flag),魔板状态队列(queue<boardTree>& treeQ),以及用以判重的访问数组(bool visit[])。

②当队列为空时结束循环并判定查找失败,否则进行循环:

a.记录队首元素(root)并使其出队。

b.利用康拓展开获得root的状态对应在全排列中的位置,并将其当做下标查看visit数组,判断root的状态是否访问过,若是则跳过当次循环。

c.判定当前路径长度是否超过限制,若是则跳过当次循环。

d.剪枝:判定当前路径的末尾是否出现“AA”或“BBBB”或“CCCC”的子串,若是则跳过当次循环。

e.若root的状态恰好是目标状态,则输出结果并跳出循环使得函数返回。

f.若非目标状态则依此将root经过A、B、C操作所得的状态推入队列。



五.程序注释清单


#include<iostream>
#include<string>
#include<string.h>
#include<queue>
using namespace std;
#define PERMUTATION_SIZE 8 // 参与全排列元素的个数
#define PERMU 40320 // 全排列总数

bool visit[PERMU]; // 访问数组
const int factory[] = { 0, 1, 2, 6, 24, 120, 720, 5040 }; // 前8个阶乘数
string startPattern = "12348765"; // 初始状态字符串

// 康拓展开函数
int cantor(string buf) {
int counted;
int result = 0;

// 找出改状态对应的全排列在所有全排列中的次序(从0开始计数)
for (int i = 0; i < PERMUTATION_SIZE; i++) {
counted = 0;

for (int j = i + 1; j < PERMUTATION_SIZE; ++j)
if (buf[i] > buf[j])
counted++;

result += counted * factory[PERMUTATION_SIZE - i - 1];
}

return result;
}

// 魔板状态数据结构:结构体
struct boardTree {
string board; // 状态对应的字符串
string path; // 路径对应的字符串
int depth; // 当前深度

boardTree() {}
boardTree(string b, string p, int d) : board(b),path(p), depth(d) {}
};

// 操作A
string operationA(string board) { string tmp = ""; int index[8] = { 4, 5, 6, 7, 0, 1, 2, 3 }; for (int i = 0; i < 8; i++) tmp += board[index[i]]; return tmp; }
// 操作B
string operationB(string board) { string tmp = ""; int index[8] = { 3, 0, 1, 2, 7, 4, 5, 6 }; for (int i = 0; i < 8; i++) tmp += board[index[i]]; return tmp; }

// 操作C
string operationC(string board) { string tmp = ""; int index[8] = { 0, 5, 1, 3, 4, 6, 2, 7 }; for (int i = 0; i < 8; i++) tmp += board[index[i]]; return tmp; }

void construct(string target, int limit, bool& flag, queue<boardTree>& treeQ, bool visit[]) {
while (!treeQ.empty()) {
// 记录队首元素(root)并使其出队
boardTree root = treeQ.front();
treeQ.pop();

// 利用康拓展开获得root的状态对应在全排列中的位置,并将其当做下标查看visit数组,判断root的状态是否访问过,若是则跳过当次循环
int tmp = cantor(root.board);
if (visit[tmp])
continue;
else
visit[tmp] = true;

// 判定当前路径长度是否超过限制,若是则跳过当次循环
if (root.depth > limit)
continue;

// 剪枝:判定当前路径的末尾是否出现“AA”或“BBBB”或“CCCC”的子串,若是则跳过当次循环
if (root.path.length() >= 2
&& (root.path[root.path.length() - 1] == 'A')
&& (root.path[root.path.length() - 2] == 'A'))
continue;

if (root.path.length() >= 4
&& (root.path[root.path.length() - 1] == 'B')
&& (root.path[root.path.length() - 2] == 'B')
&& (root.path[root.path.length() - 3] == 'B')
&& (root.path[root.path.length() - 4] == 'B'))
continue;

if (root.path.length() >= 4
&& (root.path[root.path.length() - 1] == 'C')
&& (root.path[root.path.length() - 2] == 'C')
&& (root.path[root.path.length() - 3] == 'C')
&& (root.path[root.path.length() - 4] == 'C'))
continue;

// 若root的状态恰好是目标状态,则输出结果并跳出循环使得函数返回
// 若非目标状态则依此将root经过A、B、C操作所得的状态推入队列
if (root.board == target) {
flag = true;
cout << root.depth << ' ' << root.path << endl;
return;
}
else {
boardTree left = boardTree(operationA(root.board), (root.path + "A"), root.depth + 1);
boardTree middle = boardTree(operationB(root.board), (root.path + "B"), root.depth + 1);
boardTree right = boardTree(operationC(root.board), (root.path + "C"), root.depth + 1);
treeQ.push(left);
treeQ.push(middle);
treeQ.push(right);
}
}
}

int main() {
int limit;
cin >> limit; // 输入规定路径长度限制
while (limit != -1) {
char ch; string target = "";
for (int i = 0; i < 8; i++) { // 输入初始状态字符串
cin >> ch;
target += ch;
}
if (startPattern == target) // 若初始状态等与目标状态,输出结果
cout << 0 << endl;
else { // 否则构造出事魔板状态结构体,进行construct操作
boardTree root = boardTree(startPattern, "", 0);
queue<boardTree> treeQ;
treeQ.push(root);

memset(visit, false, PERMU);

bool flag = false;
construct(target, limit, flag, treeQ, visit);

if (!flag)
cout << "-1\n";
}

cin >> limit;
}

return 0;
}


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: