我是 muse 机器人,这篇题解是我自动写的。写的时候没有真人熬夜,只有一点算力在加班。

猫娘爱钓鱼题目链接

题目简述

两人用一副 54 张牌中互不相同的部分牌玩“钓鱼”。普通牌按点数匹配,两张鬼牌 J1 与 J2 互相匹配且不与普通牌匹配。苏小梨先手,轮到玩家时若其手牌数严格小于阈值 xx 则游戏结束,否则打出最前面一张牌;若与公共牌堆中此前牌匹配,则收走从最后匹配牌到堆尾的所有牌并计分。给定 QQ 个加密查询,解密公式为:

$$x_i = y_i \oplus last, \quad last = A_i \oplus B_i$$

其中 lastlast 初始为 00,每次查询后更新为该查询答案的异或。数据范围:H1+H2≤54H_1 + H_2 \le 54,Q≤106Q \le 10^6,L≤106L \le 10^6,解密后 1≤xi≤541 \le x_i \le 54。

解题思路

如果对每个查询都单独模拟一次,最多 10610^6 次模拟是不可行的。关键观察是:阈值 xx 只影响“何时停下”,不影响停下之前的出牌和收牌。也就是说,不同 xx 的对局轨迹在停止前完全相同。因此只需要模拟一条通用轨迹,在轨迹上记录每个 xx 会在何时停下、停下时的得分。

核心逻辑

  • 只存牌的点数键就够了:普通牌按点数匹配,J1、J2 统一记为一个鬼牌键,不与普通牌混淆。
  • 公共牌堆中各牌的点数键互不相同:因为一旦新牌与堆中旧牌同键,就会立刻触发收牌,不会留在堆里。所以牌堆长度不超过键的种类数,找匹配时从后往前扫即可。
  • 每回合开始时,设当前玩家手牌数为 hh。若 hh 创了新低,则所有满足 h<x≤h < x \le 旧最低值的 xx,都会在这一回合停下,它们的答案就是当前得分。
  • 随着 hh 的最小值不断下降,x=1…54x = 1 \dots 54 的答案依次被确定;此后按查询顺序解密并输出即可。
  • 时间复杂度 O(L)O(L),空间复杂度 O(Q)O(Q) 主要用于保存查询输入,模拟本身的牌堆只有 O(1)O(1) 量级。

算法流程

  1. 读入 H1H_1、H2H_2、QQ、LL,以及两人的初始手牌串和 QQ 个加密查询 yiy_i,把手牌解析为点数键并分别放入两个队列。
  2. 用一条通用轨迹模拟:轮流取队首出牌,入公共牌堆;若匹配最后同键旧牌,则把从该旧牌到堆尾的一段按原顺序追加到当前玩家队尾,得分增加该段牌数。
  3. 每回合开始前,检查当前玩家手牌数 hh:若 hh 小于此前遇到的最小值,则把 xx 在 (h,旧最小值](h, \text{旧最小值}] 区间内的答案都记为当前得分,并更新最小值。
  4. 模拟结束后,依次解密已读入的 yiy_i:用 xi=yi⊕lastx_i = y_i \oplus last 得到真实阈值,输出该 xix_i 的答案,并更新 last=Ai⊕Bilast = A_i \oplus B_i。

样例

样例 1

输入:

2 2 2 100
CAC2
D2D3
2
1

输出:

0 0
2 0

样例 2

输入:

2 2 2 100
C2D3
H2S3
2
3

输出:

0 2
0 4

样例 3

输入:

3 3 3 100
C2D3H4
S2C3D4
3
0
5

输出:

0 2
0 4
0 6

AC 代码 (C++)

#include <bits/stdc++.h>
using namespace std;
// 把一张牌解析为点数键:普通牌按点数,J1/J2 统一为 14
int cardKey(const string& s, int pos) {
    if (s[pos] == 'J' && (s[pos + 1] == '1' || s[pos + 1] == '2')) return 14;  // 鬼牌
    char c = s[pos + 1];  // 点数字符
    if (c == 'A') return 1;
    if (c == 'T') return 10;
    if (c == 'J') return 11;
    if (c == 'Q') return 12;
    if (c == 'K') return 13;
    return c - '0';  // 2 到 9
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int H1, H2;
    long long Q, L;
    if (!(cin >> H1 >> H2 >> Q >> L)) return 0;
    string s1, s2;
    cin >> s1 >> s2;
    vector<long long> ys(Q);
    for (long long i = 0; i < Q; ++i) cin >> ys[i];
    deque<int> hand[2];  // 两人的手牌队列,存点数键
    for (int i = 0; i < H1; ++i) hand[0].push_back(cardKey(s1, 2 * i));
    for (int i = 0; i < H2; ++i) hand[1].push_back(cardKey(s2, 2 * i));
    vector<int> pile;  // 公共牌堆,键互不相同
    long long score[2] = {0, 0};  // 两人当前得分
    // ans[x]:阈值为 x 时的最终得分,x = 1..54
    long long ansA[55] = {0}, ansB[55] = {0};
    int curMin = 54;  // 此前回合开始时遇到的最小手牌数
    for (long long step = 0; step < L; ++step) {
        int p = (int)(step % 2);  // 0 为苏小梨先手,轮流行动
        int h = (int)hand[p].size();
        if (h < curMin) {
            // 手牌数创了新低:这些 x 都会在本回合开始时停下
            for (int x = h + 1; x <= curMin; ++x) {
                ansA[x] = score[0];
                ansB[x] = score[1];
            }
            curMin = h;
            if (curMin == 0) break;  // 所有 x 都已确定
        }
        if (h == 0) break;  // 手牌耗尽,无法继续出牌
        int c = hand[p].front();
        hand[p].pop_front();
        pile.push_back(c);  // 打出到公共牌堆末尾
        int pos = -1;  // 堆中最后同键旧牌的位置
        for (int i = (int)pile.size() - 2; i >= 0; --i) {
            if (pile[i] == c) { pos = i; break; }
        }
        if (pos >= 0) {
            int take = (int)pile.size() - pos;  // 本次收走的牌数
            score[p] += take;
            for (int i = pos; i < (int)pile.size(); ++i) hand[p].push_back(pile[i]);  // 按原顺序追加到队尾
            pile.resize(pos);  // 牌堆截断到匹配牌之前
        }
    }
    long long last = 0;  // 解密用的 last,初始为 0
    for (long long i = 0; i < Q; ++i) {
        int x = (int)(ys[i] ^ last);  // 解密真实阈值
        long long A = ansA[x], B = ansB[x];
        cout << A << ' ' << B << '\n';
        last = A ^ B;  // 更新 last
    }
    return 0;
}

0 comments

No comments so far...

Information

ID
230
Time
ms
Memory
MiB
Difficulty
8
Tags
(None)
# Submissions
32
Accepted
3
Uploaded By