题解:洛谷 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

【核心思想】

  1. 问题分析:魔板有 8 个格子,初始状态为 12345678,目标状态给定。允许三种操作 A(交换上下两行)、B(循环右移一列)、C(中央四格顺时针旋转)。要求找到从初始状态到目标状态的最短操作序列,且在长度最短时输出字典序最小的操作序列。本质是一个状态空间搜索问题,由于状态数有限(8! = 40320),可以使用 BFS 逐层搜索。

  2. 算法选择

    • BFS 广度优先搜索:从初始状态开始,逐层生成所有可达状态,第一次到达目标状态时路径即为最短。使用队列存储待扩展状态,使用哈希表(map<string, string>)记录每个状态对应的操作序列,同时用于去重。
    • 状态表示:将 8 个格子的数字按特定顺序拼接成一个字符串(如 12345678),便于存储和操作。
    • 操作实现:分别实现 A、B、C 三种操作的字符串变换函数,生成新状态。
    • 字典序处理:BFS 扩展时按 A、B、C 的顺序尝试操作,这样第一个到达目标状态的路径自然就是字典序最小的(因为同一层中,A 操作产生的状态先入队,优先扩展)。
  3. 关键步骤

    • 读入目标:将 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],结束搜索。
    • 输出:第一行输出操作步数(即字符串长度),第二行输出操作序列。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(8! \times 8)\),最多 40320 个状态,每个状态扩展 3 种操作,完全可行。
    • 空间复杂度:\(O(8!)\),用于存储状态和操作序列。
  5. BFS + 状态压缩的核心思想

    • 状态空间大小固定:8 个数字的全排列共 40320 种,BFS 可在合理时间内遍历所有状态。
    • 哈希表去重:使用 mapunordered_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
posted @ 2026-08-31 09:26  团爸讲算法  阅读(15)  评论(0)    收藏  举报