题解:洛谷 P2730 魔板 Magic Squares
【题目来源】
洛谷:P2730 [USACO3.2] 魔板 Magic Squares - 洛谷
【题目描述】
在成功地发明了魔方之后,鲁比克先生发明了它的二维版本,称作魔板。这是一张有 \(8\) 个大小相同的格子的魔板:
1 2 3 4
8 7 6 5
我们知道魔板的每一个方格都有一种颜色。这 \(8\) 种颜色用前 \(8\) 个正整数来表示。可以用颜色的序列来表示一种魔板状态,规定从魔板的左上角开始,沿顺时针方向依次取出整数,构成一个颜色序列。对于上图的魔板状态,我们用序列 \({1,2,3,4,5,6,7,8}\) 来表示。这是基本状态。
这里提供三种基本操作,分别用大写字母 \(A\),\(B\),\(C\) 来表示(可以通过这些操作改变魔板的状态):
\(A\):交换上下两行;
\(B\):将最右边的一列插入最左边;
\(C\):魔板中央四格作顺时针旋转。
下面是对基本状态进行操作的示范:
\(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
对于每种可能的状态,这三种基本操作都可以使用。
你要编程计算用最少的基本操作完成基本状态到目标状态的转换,输出基本操作序列。
【输入】
只有一行,包括 \(8\) 个整数 \(a_1,a_2,\dots,a_8(1\le a_1,a_2,\dots,a_8\le 8)\),用空格分开,不换行,表示目标状态。
【输出】
第一行包括一个整数,表示最短操作序列的长度。
第二行在字典序中最早出现的操作序列,用字符串表示,除最后一行外,每行输出 \(60\) 个字符。
【输入样例】
2 6 8 4 5 7 3 1
【输出样例】
7
BCABCCB
【核心思想】
-
问题分析:魔板有 8 个格子,初始状态为
12345678,目标状态给定。允许三种操作 A(交换上下两行)、B(循环右移一列)、C(中央四格顺时针旋转)。要求找到从初始状态到目标状态的最短操作序列,且在长度最短时输出字典序最小的操作序列。本质是一个状态空间搜索问题,由于状态数有限(8! = 40320),可以使用 BFS 逐层搜索。 -
算法选择:
- BFS 广度优先搜索:从初始状态开始,逐层生成所有可达状态,第一次到达目标状态时路径即为最短。使用队列存储待扩展状态,使用哈希表(
map<string, string>)记录每个状态对应的操作序列,同时用于去重。 - 状态表示:将 8 个格子的数字按特定顺序拼接成一个字符串(如
12345678),便于存储和操作。 - 操作实现:分别实现 A、B、C 三种操作的字符串变换函数,生成新状态。
- 字典序处理:BFS 扩展时按 A、B、C 的顺序尝试操作,这样第一个到达目标状态的路径自然就是字典序最小的(因为同一层中,A 操作产生的状态先入队,优先扩展)。
- BFS 广度优先搜索:从初始状态开始,逐层生成所有可达状态,第一次到达目标状态时路径即为最短。使用队列存储待扩展状态,使用哈希表(
-
关键步骤:
- 读入目标:将 8 个整数拼接成目标字符串
ed。 - BFS 初始化:队列
q放入初始状态"12345678",哈希表mp记录mp["12345678"] = ""。 - BFS 循环:
- 取出队首状态
s。 - 依次尝试 A、B、C 操作生成新状态
t。 - 若
t未在mp中,则将其入队,并记录操作序列mp[t] = mp[s] + 操作字符。 - 若
t == ed,则输出mp[t].size()和mp[t],结束搜索。
- 取出队首状态
- 输出:第一行输出操作步数(即字符串长度),第二行输出操作序列。
- 读入目标:将 8 个整数拼接成目标字符串
-
时间/空间复杂度:
- 时间复杂度:\(O(8! \times 8)\),最多 40320 个状态,每个状态扩展 3 种操作,完全可行。
- 空间复杂度:\(O(8!)\),用于存储状态和操作序列。
-
BFS + 状态压缩的核心思想:
- 状态空间大小固定:8 个数字的全排列共 40320 种,BFS 可在合理时间内遍历所有状态。
- 哈希表去重:使用
map或unordered_map记录已访问状态及其路径,避免重复扩展,同时存储路径。 - 最短路径保证:BFS 按层扩展,首次到达目标状态的路径一定最短。
- 字典序最小:按 A、B、C 的顺序尝试操作,且 BFS 队列先进先出,自然保证同一深度下先扩展的操作序列字典序更小。
- 适用场景:这类“状态空间小、操作确定、求最短路径”的问题非常适合 BFS,如拼图、魔方简化版等。
【算法标签】
普及+ #BFS-一维
【代码详解】
#include <bits/stdc++.h>
using namespace std;
string ed; // 目标状态字符串
queue<string> q; // BFS队列,存储待处理的状态
map<string, string> mp; // 哈希表,记录每个状态对应的操作序列
/**
* 执行A操作:交换字符串的前后两半
* @param x 当前状态字符串
*/
void moveA(string x)
{
string y = x; // 保存原始状态
// 交换前4个字符和后4个字符
for (int i = 0; i < 4; i++)
{
swap(x[i], x[7 - i]);
}
// 如果新状态未被访问过
if (mp.count(x) == 0)
{
q.push(x); // 加入队列
mp[x] = mp[y] + 'A'; // 记录操作序列
}
}
/**
* 执行B操作:将中间四个字符顺时针旋转
* @param x 当前状态字符串
*/
void moveB(string x)
{
string y = x; // 保存原始状态
// 执行B操作:顺时针旋转中间四个字符
x[0] = y[3], x[1] = y[0], x[2] = y[1], x[3] = y[2];
x[4] = y[5], x[5] = y[6], x[6] = y[7], x[7] = y[4];
// 如果新状态未被访问过
if (mp.count(x) == 0)
{
q.push(x); // 加入队列
mp[x] = mp[y] + 'B'; // 记录操作序列
}
}
/**
* 执行C操作:交换特定位置的字符
* @param x 当前状态字符串
*/
void moveC(string x)
{
string y = x; // 保存原始状态
// 执行C操作:交换特定位置的字符
x[1] = y[6], x[2] = y[1], x[5] = y[2], x[6] = y[5];
// 如果新状态未被访问过
if (mp.count(x) == 0)
{
q.push(x); // 加入队列
mp[x] = mp[y] + 'C'; // 记录操作序列
}
}
/**
* 广度优先搜索主函数
*/
void bfs()
{
q.push("12345678"); // 初始状态入队
mp["12345678"] = ""; // 初始状态对应空操作序列
while (q.size())
{
string s = q.front(); // 取出队首状态
q.pop();
// 尝试三种操作
moveA(s);
moveB(s);
moveC(s);
// 如果找到目标状态
if (mp.count(ed))
{
// 输出操作步数和操作序列
cout << mp[ed].size() << endl << mp[ed];
return;
}
}
}
int main()
{
char c;
// 读取目标状态
for (int i = 1; i <= 8; i++)
{
cin >> c;
ed += c;
}
// 执行广度优先搜索
bfs();
return 0;
}
【运行结果】
2 6 8 4 5 7 3 1
7
BCABCCB
浙公网安备 33010602011771号