洛谷P2730 魔板 Magic Squares

来源:互联网 发布:詹姆斯身体素质知乎 编辑:程序博客网 时间:2024/06/04 18:49

题目背景

  • 在成功地发明了魔方之后,鲁比克先生发明了它的二维版本,称作魔板。这是一张有8个大小相同的格子的魔板:
    |1|2|3|4|
    |8|7|6|5|

题目描述

  • 我们知道魔板的每一个方格都有一种颜色。这8种颜色用前8个正整数来表示。可以用颜色的序列来表示一种魔板状态,规定从魔板的左上角开始,沿顺时针方向依次取出整数,构成一个颜色序列。对于上图的魔板状态,我们用序列(1,2,3,4,5,6,7,8)来表示。这是基本状态
  • 这里提供三种基本操作,分别用大写字母“A”,“B”,“C”来表示(可以通过这些操作改变魔板的状态):
    1. “A”:交换上下两行;
    2. “B”:将最右边的一列插入最左边;
    3. “C”:魔板中央四格作顺时针旋转。
  • 下面是对基本状态进行操作的示范:
    1. A:
      |8|7|6|5|
      |1|2|3|4|
    2. B:
      |4|1|2|3|
      |5|8|7|6|
    3. C:
      |1|7|2|4|
      |8|6|3|5|
  • 对于每种可能的状态,这三种基本操作都可以使用。
  • 你要编程计算用最少的基本操作完成基本状态到目标状态的转换,输出基本操作序列。

输入输出格式

输入格式

  • 只有一行,包括8个整数,用空格分开(这些整数在范围 1——8 之间)不换行,表示目标状态。

输出格式

  • Line 1: 包括一个整数,表示最短操作序列的长度。
  • Line 2: 在字典序中最早出现的操作序列,用字符串表示,除最后一行外,每行输出60个字符。

输入输出样例

输入样例#1

  • 2 6 8 4 5 7 3 1

输出样例#1

  • 7
    BCABCCB

说明

  • 题目翻译来自NOCOW。
    USACO Training Section 3.2

原题地址

  • 洛谷P2730 BZOJ1331

分析 哈希表 + 广度优先搜索

  • 看到这样的题目应该很容易想到搜索,但如果用深度优先搜索做,则可能会一直无法变换到目标状态,从而导致死循环,所以我们只能用广度优先搜索,那么最先搜到的步数肯定最少。而问题的关键在于如何对已经搜索过的状态进行判重,于是对于这一类的问题,我们很容易想到哈希表。
  • 考虑一种最为简单的思路,把一种状态中的八个数字对应转换成八位十进制数上的某一位上,但这样是存储不下的。我们想到除余法,发现共有全排列8!=40320个情况。于是我们设的模数大概是比40320稍大的素数,然后将每一种情况对应到哈希表中。(膜拜用康托展开做的大神)
  • 鉴于本题的特殊性,其实我们还可以考虑另一种较为简便的哈希函数,因为状态只包含这八个数字,我们统一令其减一,则我们就可以将这八个数字作为一个八位八进制数上的每一位,也就是基数转换法
  • 这样做的优点在哪呢?我们会发现最大的状态(76543210)8=(16434824)10,那么我们只要开一个大约1650万的bool数组就可以直接用来判重了,而不需要再使用除余法等等的操作。并且每一种状态是唯一的,也不可能发生冲突的情况,另外转成八进制数的乘以8也可以用位运算中的右移来快速实现。

代码

#include <iostream>#include <cstdio>using namespace std;const int N = 5e4 + 5, M = 165e5;const int G[3][9] = {{0, 8, 7, 6, 5, 4, 3, 2, 1},                      //操作A:交换上下两行                      {0, 4, 1, 2, 3, 6, 7, 8, 5},                      //操作B:将最右边的一列插入最左边                      {0, 1, 7, 2, 4, 5, 3, 6, 8}};                     //操作C:魔板中央四格作顺时针旋转 //我们可以用一个常量数组来简单地表示ABC三种操作 //即经过这一次操作后,现在状态中的第i个位置是有原先的哪一个位置变换来的 int h[N][9], a[9], pf[N][2]; char stk[N];int t, w = 1, x, edt, now, top;bool vis[M]; //用于判重的bool数组 int main(){    for (int i = 1; i <= 8; ++i)     scanf("%d", &x), edt = (edt << 3) + x - 1;    //同样将目标状态转换为八进制数,便于我们直接判断     for (int i = 1; i <= 8; ++i)      now = (now << 3) + (h[1][i] = i - 1);    //“<< 3”即表示位运算的右移三位,也就是乘以8     vis[now] = true; //注意初始状态也要标记为已经搜索过     if (now == edt) return puts("0"), 0;    //注意如果初始状态和目标状态相同应直接退出     while ((t++) < w)    {        for (int i = 0; i < 3; ++i)        //对于答案中的操作字典序问题,我们考虑按照操作ABC的顺序搜索        //这样先搜索到的一定是字典序最小的         {            now = 0;            for (int j = 1; j <= 8; ++j)              now = (now << 3) + (a[j] = h[t][G[i][j]]);            //计算经过转换后的状态             if (vis[now]) continue; //判重             vis[now] = true;            pf[++w][0] = t; pf[w][1] = i + 'A';            //因为题目中要求输出操作序列            //记录队列中每一个元素是由之前的哪一个元素、经过哪一个操作转换来的            //然后按着当前搜到的目标状态倒着找回去,再顺着输出操作就是答案了             h[w][0] = h[t][0] + 1;            for (int j = 1; j <= 8; ++j) h[w][j] = a[j];            if (now == edt) //转换到了目标状态             {                printf("%d\n", h[w][0]); x = w;                while (pf[x][0]) stk[++top] = pf[x][1], x = pf[x][0];                //数组stk即记录转换到目标状态依次进行的操作                 for (int i = top; i >= 1; --i) putchar(stk[i]);                return 0;            }        }    }    return 0;}
原创粉丝点击