题目链接在此
经典的状态转换搜索题。
一.原题中文大意
(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]来记录。
①使用广度优先搜索算法搜索状态;
②用康拓展开压缩状态,将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]*(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;
}