[luogu p1213] [USACO1.4][IOI1994]时钟 The Clocks

\(\mathtt{Link}\)

P1213 [USACO1.4][IOI1994]时钟 The Clocks - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

\(\mathtt{Description}\)

给你九个表的初始指向(只有12点,3点,6点,9点四种指向),九种操作,每种操作可以将某些表(题中给出)依次顺时针旋转 90 度,问可以使所有表均指向12点的长度最短(操作数最少)且操作字典序最小的操作。

\(\mathtt{Data} \text{ } \mathtt{Range} \text{ } \mathtt{\&} \text{ } \mathtt{Restrictions}\)

没有

\(\mathtt{Solution}\)

这个题一眼枚举,普通的做法是搜索,也有一些奇怪的乱搞做法,我这里写了一种介于普通和奇怪之间的做法——二进制状压。

首先看题目条件,分析出一下这几个信息:

  • 所有表所有可能情况仅有 $4 ^9 $ 种;

  • 需要考虑的所有操作情况仅有 \(4^9\) 种。

第一种是显然的(根据乘法原理),第二种是因为:当你对一个操作施加了四次之后,跟不施加它是没有任何区别的(所有指针恰好转了一圈)

四的九次方算一下大概是二十多万,同时我们考虑到这是四进制,只要把四进制中的每一位拆成二进制就可以了,然后就能状压了。

首先具体来说怎么状压表的状态。我们用一个十八位二进制数,每两位表示一个表的情况,这里我们规定:3 -> 01(1),6 -> 10(2),9 -> 11(3),12 -> 00(0),于是这样初始状态就表示好了,举个例子,样例的初始状态就是 158351,为了直观理解,我用二进制表示这个数,并且和指针和表编号对应:(下面的“嗯”是占位用的)

二进制:10 01 10 10 10 10 00 11 11

嗯指针:06 03 06 06 06 06 12 09 09

表编号:09 08 07 06 05 04 03 02 01

看明白了吗?反正我是看明白了。。

现在再来说怎么状压操作序列。同样用一个十八位二进制数,每两位表示一个操作的情况,这两位的值就表示这个操作进行多少次。诶那么我们就不妨再次用一下这个数,来看一下他表示操作会对应什么:

二进制:10 01 10 10 10 10 00 11 11

十进制:02 01 02 02 02 02 00 03 03

操作号:09 08 07 06 05 04 03 02 01

也就是说 158351 翻译为序列操作情况就是:进行A操作3次;进行B操作3次;不进行C操作;进行D操作2次;进行E操作2次;进行F操作2次;进行G操作2次;进行H操作1次;进行I操作2次。


现在明白了怎么状压,那么我们再来看怎么将序列操作翻译过来并对表操作?

流程图:操作序列状压码 -> 具体操作情况 -> 具体对表的旋转情况 -> 转表

第一步到第二步:设当前枚举到的操作序列状压码是 i,然后 j 从 0 -> 8 循环(表示要取 j + 1 号操作的次数),次数为:(i >> (j << 1)) & 3

如何理解?把操作序列状压码右移 j << 1 == j * 2,然后取两位(& 3),于是我们获得了原状压码的 j * 2j * 2 + 1 位(从右到左数哦!)拼起来的值,这就是操作次数了。

有了操作号 j + 操作次数(上面表示的),我们就破译出了具体操作情况。

第二步到第三步:这个时候我们就必须打表出来操作情况了,题目赤裸裸给出,我们也赤裸裸的抄上。

const int d[9][5] = {
    {1, 2, 4, 5, 0},
    {1, 2, 3, 0, 0},
    {2, 3, 5, 6, 0},
    {1, 4, 7, 0, 0},
    {2, 4, 5, 6, 8},
    {3, 6, 9, 0, 0},
    {4, 5, 7, 8, 0},
    {7, 8, 9, 0, 0},
    {5, 6, 8, 9, 0}
};

于是直接通过循环访问 d 数组我们得到了具体要转的表情况。

第三步到第四步:转表。我们先看对于一个两位二进制数表示的表指针,顺时针九十度是如何操作的:

01 -> 10, 10 -> 11, 11 -> 00, 00 -> 01

对每一位看,我们能发现:

第一位(从右到左数!!)每次一定取反;

第二位在第一位是1的时候,取反;否则不取反。

根据这个规律写下伪代码:

for (int k = 0; k < 5; ++k) {
    //printf("%d\n", _);
    if (d[j][k] == 0)
        break;
    int p = (d[j][k] - 1) << 1; // p 代表钟表所在的后一位
    for (int _ = 0; _ < t; ++_) {
        if (now & (1 << p)) // 如果后一位是1
            now ^= 1 << (p | 1); // 反转前一位
        now ^= 1 << p; // 无论如何都要反转后一位
    }
}

接下来我们就得到了now——最后的表盘情况。我们要检验表盘是否都指向12指针了:只需要看它等不等于0。于是你就应该明白当时我为什么要把12点设计成0了。

由于我们状压正序枚举,先枚举到的所对应的操作序列一定字典序最小。但是我们要注意,这样获得的序列可能不是最短的。

为了解决这个问题我们可以比较一下新的合法操作序列长度和之前得到过的操作序列长度,判断一下再更新就可以了。

\(\mathtt{Time} \text{ } \mathtt{Complexity}\)

\(\operatorname{O}(4 ^ n)\)

\(n = 9\)

\(\mathtt{Code}\)

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2022-07-03 16:16:05 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2022-07-03 17:56:39
 */
#include <bits/stdc++.h>
inline int read() {
    int x = 0;
    bool flag = true;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-')
            flag = false;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = (x << 1) + (x << 3) + ch - '0';
        ch = getchar();
    }
    if(flag) return x;
    return ~(x - 1);
}

const int d[9][5] = {
    {1, 2, 4, 5, 0},
    {1, 2, 3, 0, 0},
    {2, 3, 5, 6, 0},
    {1, 4, 7, 0, 0},
    {2, 4, 5, 6, 8},
    {3, 6, 9, 0, 0},
    {4, 5, 7, 8, 0},
    {7, 8, 9, 0, 0},
    {5, 6, 8, 9, 0}
};

std :: basic_string <int> sol;

int main() {
    int state = 0;
    for (int i = 0; i < 9; ++i) {
        int x = read() / 3 & 3; // 3 6 9 12 -> 1 2 3 0 -> 01 10 11 00
        state |= x << (i << 1); 
    }


    for (int i = 0; i < 1 << 19; ++i) {
        int now = state;
        for (int j = 0; j < 9; ++j) {
            int t = (i >> (j << 1)) & 3;
            if (t == 0)
                continue; // 优化
            for (int k = 0; k < 5; ++k) {
                //printf("%d\n", _);
                if (d[j][k] == 0)
                    break;
                int p = (d[j][k] - 1) << 1; // p 代表钟表所在的后一位
                for (int _ = 0; _ < t; ++_) {
                    if (now & (1 << p)) // 如果后一位是1
                        now ^= 1 << (p | 1); // 反转前一位
                    now ^= 1 << p; // 无论如何都要反转后一位
                }
            }
        }
        if (now == 0) {
            std :: basic_string <int> now_sol;
            now_sol.clear();
            for (int j = 0; j < 9; ++j)
                for (int _ = 0; _ < ((i >> (j << 1)) & 3); ++_) // 意义同上
                    now_sol += j + 1;
            if (sol.empty() || now_sol.size() < sol.size())
                sol = now_sol;
            break;
        }
    }
    
    for (int i = 0; i < sol.size(); ++i)
        printf("%d ", sol[i]);
    puts("");
    return 0;
}

/*
9 9 12
6 6 6
6 3 6 
*/

适合初学搜索的人做,当然也不乏是一道二进制状压枚举好题!

posted @ 2022-07-03 18:32  dbxxx  阅读(143)  评论(1编辑  收藏  举报