usaco-1.4.1 时钟

GitHub跳转原题关系图返回列表

每种移动做 4 次等于没做,只需枚举 4^9 个次数向量;影响矩阵模 4 可逆,解唯一,找到即可输出。

OJ: roj

题目 ID: 2013

难度:普及-

标签:枚举位运算模拟python

创建: 2026-10-01 02:55

更新: 2026-10-06 09:33

题目描述

九只钟排成 3×33 \times 3 方阵(A~I),指针只有 12/3/6/9 点四种位置。有 9 种移动,第 mm 种把它影响的钟顺时针拨 90°90°:1 ABDE,2 ABC,3 BCEF,4 ADG,5 BDEFH,6 CFI,7 DEGH,8 GHI,9 EFHI。输入 3 行各 3 个数表示初始时间(3/6/9/12);输出一行空格分开的最短移动序列使所有指针指向 12 点,多种方案时输出数字拼接最小的。样例输入 9 9 12 / 6 6 6 / 6 3 6,样例输出 4 5 8 9。

思路

每种移动做 4 次等于没做,把 9 种移动各做几次(0~3)编码成 18 位四进制数,枚举全部 49=2621444^9 = 262144 个次数向量,检验每只钟"初值 + 被拨次数"是否模 4 为 0。又因九种移动的影响矩阵在模 4 下可逆(det⁡=5\det = 5 为奇数),每个初态恰有一个合法次数向量,找到第一个可行解直接输出即可。

参考代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 02:55
# update_at: 2026-10-01 03:13

import sys
from functools import cache
from itertools import product

WHEEL = 4       # 指针只有四种位置:0 = 12 点,1 = 3 点,2 = 6 点,3 = 9 点
FIELD = 0b11    # 每个时钟用一个两位字段存指针位置,取值 0..3
SHIFT = [5 * i for i in range(9)]       # 第 i 只钟的状态在状态整数中的起始位
UNIT = [1 << shift for shift in SHIFT]  # 把某只钟加 1(顺时针转 90 度)对应的加数
LOW = sum(FIELD << shift for shift in SHIFT)    # 九个位置字段的掩码:结果全为 0 即是大功告成

# 九种移动各自影响的时钟下标,顺序与题面表格一致:1 ABDE, 2 ABC, 3 BCEF, 4 ADG,
# 5 BDEFH, 6 CFI, 7 DEGH, 8 GHI, 9 EFHI
GROUP = [(0, 1, 3, 4), (0, 1, 2), (1, 2, 4, 5), (0, 3, 6), (1, 3, 4, 5, 7),
         (2, 5, 8), (3, 4, 6, 7), (6, 7, 8), (4, 5, 7, 8)]
# 施加一次第 i 种移动,等于给受影响的每只钟各加 1(指针顺时针转 90 度)
STEP = [sum(UNIT[i] for i in g) for g in GROUP]


@cache
def plan(state: int) -> tuple[int, ...]:
    """求把 state 拨成九个 12 点所需的次数向量 c:c[i] 是移动 i+1 要做几次。

    时钟只有 4 种状态,移动 i 做 4 次等于没做,所以 c[i] 只需枚举 0..3,共 4^9 个候选。
    设影响矩阵 V(V[i][m] = 1 表示移动 m 拨动第 i 只钟),则 det(V) = 5 是奇数,
    V 在模 4 下可逆,所以候选与结果状态一一对应(枚举验证:4^9 个候选恰好得到 4^9 个
    互不相同的状态)。**每个初态恰有一个合法次数向量**,题面「多种方案取数字拼接最小」
    永远不会触发,因此取第一个可行解即可,长度和字典序都不必比较。

    状态里每只钟预留 5 位,而它最多被 5 种移动碰到(每种最多 3 次,和不超过 15),
    相加不会进位串到相邻字段;于是求和后只看 LOW 就能判断九只钟是否同时指向 12 点。
    """
    return next((c for c in product(range(WHEEL), repeat=len(GROUP))
                 if (state + sum(k * s for k, s in zip(c, STEP))) & LOW == 0), ())


def solve() -> None:
    times = map(int, sys.stdin.buffer.read().split())
    # 12 点记为 0,于是初始状态 = 时间 / 3 mod 4;把九个时钟拼进一个整数
    state = sum((t // 3 % WHEEL) << shift for t, shift in zip(times, SHIFT))
    print(' '.join(str(move) for move, k in enumerate(plan(state), 1) for _ in range(k)))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-06 09:33
 * update_at: 2026-10-06 09:33
 */
#include <cstdio>

typedef long long ll;

// 九种移动各自拨动的时钟集合,时钟按 A~I 编号 0~8,用二进制位记录
// 1 ABDE  2 ABC  3 BCEF  4 ADG  5 BDEFH  6 CFI  7 DEGH  8 GHI  9 EFHI
const int msk[10] = {0, 27, 7, 54, 73, 186, 292, 216, 448, 432};

int t[9];   // t[i] 表示第 i 只钟的初始位置:12/3/6/9 记 0/1/2/3
int c[10];  // c[m] 表示第 m 种移动做几次(0~3,做 4 次等于没做)

// 检验次数向量 c[1..9] 能否把九只钟都拨回 12 点:每只钟初值加被拨次数模 4 为 0
bool check() {
    for (int i = 0; i < 9; i++) {
        int sum = t[i];
        for (int m = 1; m <= 9; m++)
            if ((msk[m] >> i) & 1) sum += c[m];
        if (sum % 4 != 0) return false;
    }
    return true;
}

// 把次数向量展开成移动序列输出
void print_ans() {
    bool first = true;
    for (int m = 1; m <= 9; m++)
        for (int k = 0; k < c[m]; k++) {
            if (!first) printf(" ");
            first = false;
            printf("%d", m);
        }
    printf("\n");
}

int main() {
    // 读入九只钟的初始时间并转换:12->0 3->1 6->2 9->3,即 (时间/3)%4
    for (int i = 0; i < 9; i++) {
        int x;
        scanf("%d", &x);
        t[i] = x / 3 % 4;
    }

    // 把 9 种移动各做几次编码成 18 位四进制数,枚举全部 4^9 = 262144 个候选
    // 影响矩阵模 4 可逆(det = 5 为奇数),每个初态恰有一个解,找到即可输出
    for (int code = 0; code < 262144; code++) {
        for (int m = 1; m <= 9; m++)
            c[m] = (code >> (2 * (m - 1))) & 3;
        if (check()) {
            print_ans();
            return 0;
        }
    }
    return 0;
}