[NOIP 2014 提高组] 生活大爆炸版石头剪刀布 的题解


记住只在没有思路时使用题解,不要从它复制粘贴代码。请尊重题目和题解的作者。
在解题之前提交题解的代码会导致封禁。

作者: admin

概述

本题要求按照双方给定的出拳周期模拟 N 局升级版石头剪刀布,分别统计小 A 与小 B 的得分。核心解法是先把题面表中五种手势的胜负关系固化成一张 5 \times 5 的常量表,再按各自周期取模逐局查表累加。

分析
核心观察

每一局的胜负只由双方该局出的手势决定,与之前的局面无关,因此 N 局的得分可以逐局独立累加。双方手势的组合一共只有 5 \times 5 = 25 种,把这 25 种组合的胜负预先存成一张表,单局就退化为一次数组访问。

思路

用 a_0, a_1, \dots, a_{N_A - 1} 记小 A 的周期,用 b_0, b_1, \dots, b_{N_B - 1} 记小 B 的周期。两个周期各自循环,故第 k 局(从 1 数起)两人出的手势为

\displaystyle  a_{(k-1) \bmod N_A}, \qquad b_{(k-1) \bmod N_B}

两者的周期长度不一定相等,整个出拳序列的最短公共周期是 \mathrm{lcm}(N_A, N_B),但直接模拟 N 局已经足够快,无需利用这一点。

令 w(x, y) 表示甲出 x、乙出 y 时甲的得分,赢为 1,平或负为 0,其取值就是题面表中的每一格。第 k 局中甲是小 A、乙是小 B,故小 A 得 w(a, b) 分、小 B 得 w(b, a) 分。对任意两个手势 x \ne y,必有一方获胜而另一方落败,因此 w(x, y) + w(y, x) = 1;题面表中留白的下三角正是靠这一关系补全的。当 x = y 时两式都取 0,恰好对应平局时两人都不得分,无需特判。

朴素做法是每局现场判断双方手势的克制关系,需要常数次比较,总复杂度同样是 O(N);查表把单局开销压缩到一次数组访问,同时避免了现场判断写错方向。

具体示例

以样例 1(N = 10,N_A = 5,N_B = 6)为例。小 A 的周期是 0 1 2 3 4,即 剪刀、石头、布、蜥蜴人、斯波克;小 B 的周期是 0 3 4 2 1 0。逐局查表的结果如下。

局数 小 A 小 B 结果 小 A 得分 小 B 得分
1 剪刀 剪刀 平 0 0
2 石头 蜥蜴人 小 A 赢 1 0
3 布 斯波克 小 A 赢 2 0
4 蜥蜴人 布 小 A 赢 3 0
5 斯波克 石头 小 A 赢 4 0
6 剪刀 剪刀 平 4 0
7 石头 剪刀 小 A 赢 5 0
8 布 蜥蜴人 小 B 赢 5 1
9 蜥蜴人 斯波克 小 A 赢 6 1
10 斯波克 布 小 B 赢 6 2

前 5 局小 A 只在第 1 局与对手打平,其余 4 局全胜,比分来到 4 : 0;第 6、7 局小 A 又拿 1 分,第 8 局小 B 拿到本场第 1 分,第 9 局小 A 得分到 6,第 10 局小 B 再得 1 分。终局比分 6 : 2,与样例输出一致。

算法步骤
  1. 把题面表中的胜负关系写成常量二维数组 beat,beat[x][y] 取 1 表示甲出 x、乙出 y 时甲赢,取 0 表示平局或甲输;留白的下三角按 w(x, y) + w(y, x) = 1 补出。
  2. 读入局数 n 与两个周期长度 na、nb。
  3. 读入小 A 的周期数组 a[0 .. na - 1] 与小 B 的周期数组 b[0 .. nb - 1]。
  4. 令小 A 的得分 scoreA 与小 B 的得分 scoreB 的初值为 0。
  5. 对每一局下标 i(0 \le i < n)取 x 为 a[i % na]、y 为 b[i % nb],把 scoreA 加上 beat[x][y],把 scoreB 加上 beat[y][x]。
  6. 输出 scoreA 与 scoreB,两者之间用一个空格分隔。
复杂度分析
  • 时间:O(N)。胜负表是常量,每局只做两次取模、两次数组访问与两次加法。
  • 空间:O(N_A + N_B),只需保存两个周期数组,胜负表只占 25 个整数。
实现注意事项
  • 胜负表必须与题面表一逐格对齐。表中下三角留白,非平局的另一格由 w(x, y) + w(y, x) = 1 补出;凭“五种手势大致怎么克制”的印象填写容易出错,例如 石头 对 斯波克 是输而不是赢。
  • 下标从 0 开始,第 k 局对应 (k-1) \bmod N_A 与 (k-1) \bmod N_B,直接用 i 对周期长度取模即可。
  • 两人得分分别用 beat[x][y] 与 beat[y][x] 累加,方向不能写反,也不能对同一格重复累加。
  • 平局时两人都不得分,而表中平局格的值已是 0,不需要额外特判。
  • N、N_A、N_B 都不超过 200,单方得分不超过 200,int 足够。
  • 输入共三行,规模很小,用 cin 或一次性读入后切分即可,无需额外优化。
  • 输出两项之间是一个空格,行末换行。
源代码
#include <iostream>
#include <vector>

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    // beat[i][j] = 1 表示甲出 i、乙出 j 时甲获胜,0 表示甲平或负
    static const int beat[5][5] = {
        {0, 0, 1, 1, 0},  // 剪刀
        {1, 0, 0, 1, 0},  // 石头
        {0, 1, 0, 0, 1},  // 布
        {0, 0, 1, 0, 1},  // 蜥蜴人
        {1, 1, 0, 0, 0},  // 斯波克
    };

    int n = 0, na = 0, nb = 0;
    if (!(std::cin >> n >> na >> nb)) return 0;

    std::vector<int> a(na), b(nb);
    for (int i = 0; i < na; ++i) std::cin >> a[i];
    for (int i = 0; i < nb; ++i) std::cin >> b[i];

    int scoreA = 0, scoreB = 0;
    for (int i = 0; i < n; ++i) {
        const int x = a[i % na];
        const int y = b[i % nb];
        scoreA += beat[x][y];
        scoreB += beat[y][x];
    }

    std::cout << scoreA << ' ' << scoreB << '\n';
    return 0;
}
import sys

# BEAT[i][j] == 1 表示甲出 i、乙出 j 时甲获胜,0 表示甲平或负
BEAT = (
    (0, 0, 1, 1, 0),  # 剪刀
    (1, 0, 0, 1, 0),  # 石头
    (0, 1, 0, 0, 1),  # 布
    (0, 0, 1, 0, 1),  # 蜥蜴人
    (1, 1, 0, 0, 0),  # 斯波克
)


def main():
    data = sys.stdin.buffer.read().split()
    if not data:
        return
    n, na, nb = int(data[0]), int(data[1]), int(data[2])
    a = [int(v) for v in data[3:3 + na]]
    b = [int(v) for v in data[3 + na:3 + na + nb]]

    score_a = 0
    score_b = 0
    for i in range(n):
        x = a[i % na]
        y = b[i % nb]
        score_a += BEAT[x][y]
        score_b += BEAT[y][x]

    sys.stdout.write("%d %d\n" % (score_a, score_b))


if __name__ == "__main__":
    main()

评论

目前没有评论。