三国杀I(洗牌&发牌)

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

直接按题目规则模拟 m 次洗牌,再从洗完后的牌堆中按发牌位置取出第 p 个玩家的 4 张牌。

OJ: luogu

题目 ID: P2348

难度:入门

标签:模拟

日期: 2026-06-19 03:02

题意

n 个玩家、k 张牌,每个玩家需要发 4 张牌。

整副牌先按题目给定的规则洗 m 次,再从上到下依次发牌:

  • 1 张发给 1 号玩家;
  • 2 张发给 2 号玩家;
  • n+1 张又回到 1 号玩家。

现在只问第 p 位玩家最后拿到的 4 张牌。

如果总牌数不够给所有玩家每人 4 张,就输出:

Error:cards not enough

思路

这题没有隐藏算法,本质就是一道直接模拟题。

先看最直观的写法:每次洗牌都新建一个牌堆,然后严格按题目给出的顺序把旧牌堆重排过去。

这个版本最容易理解:

cpp
#include <bits/stdc++.h>
using namespace std;

struct Card {
    string suit_point;
    string kind;
};

int n, k, m, p;
vector<Card> deck;

vector<Card> shuffle_once(const vector<Card> &cur) {
    int len = (int) cur.size();
    int half = len / 2;
    vector<Card> nxt;

    // 朴素做法:直接按题目给出的顺序重新构造一副新牌。
    for (int i = 0; i < half; i++) {
        nxt.push_back(cur[half + i]);
        nxt.push_back(cur[i]);
    }

    if (len % 2 == 1) {
        nxt.push_back(cur[len - 1]);
    }

    return nxt;
}

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

    cin >> n >> k >> m >> p;
    deck.resize(k);
    for (int i = 0; i < k; i++) {
        cin >> deck[i].suit_point >> deck[i].kind;
    }

    if (k < 4 * n) {
        cout << "Error:cards not enough\n";
        return 0;
    }

    for (int i = 1; i <= m; i++) {
        deck = shuffle_once(deck);
    }

    for (int i = p - 1, cnt = 0; i < k && cnt < 4; i += n, cnt++) {
        cout << deck[i].suit_point << ' ' << deck[i].kind << '\n';
    }

    return 0;
}

一次洗牌怎么做

设当前牌堆从上到下编号为:

1, 2, 3, ..., k

题目说一次洗牌后顺序变成:

floor(k/2)+1, 1, floor(k/2)+2, 2, ...

这说明我们可以把牌堆分成两部分:

  • 前半段:1..floor(k/2)
  • 后半段:floor(k/2)+1..k

然后按“后半一张、前半一张”的顺序交错放到新牌堆里。

如果 k 是奇数,后半段会多出最后一张牌,它就直接落到新牌堆末尾。

为什么发牌位置很好找

洗牌全部完成后,再开始发牌。

由于发牌始终按玩家编号循环,所以第 p 位玩家拿到的一定是最终牌堆中的:

  • p 张;
  • p+n 张;
  • p+2n 张;
  • p+3n 张。

因此我们根本不需要再单独模拟“谁拿了哪张”,直接按下标跳着取就行。

正式做法

  1. k < 4n,直接输出错误信息;
  2. 否则模拟 m 次洗牌;
  3. 最后输出位置 p, p+n, p+2n, p+3n 上的牌。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXK = 100000 + 5;

struct Card {
    string suit_point;
    string kind;
};

int n, k, m, p;
Card a[MAXK], b[MAXK];

void shuffle_once() {
    int half = k / 2;
    int pos = 0;

    // 新牌堆按“后半张、前半张”交错放入。
    for (int i = 1; i <= half; i++) {
        b[++pos] = a[half + i];
        b[++pos] = a[i];
    }

    if (k % 2 == 1) {
        b[++pos] = a[k];
    }

    for (int i = 1; i <= k; i++) {
        a[i] = b[i];
    }
}

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

    cin >> n >> k >> m >> p;
    for (int i = 1; i <= k; i++) {
        cin >> a[i].suit_point >> a[i].kind;
    }

    if (k < 4 * n) {
        cout << "Error:cards not enough\n";
        return 0;
    }

    for (int i = 1; i <= m; i++) {
        shuffle_once();
    }

    int need = 0;
    for (int i = p; i <= k && need < 4; i += n) {
        cout << a[i].suit_point << ' ' << a[i].kind << '\n';
        need++;
    }

    return 0;
}

复杂度

  • 时间复杂度:O(mk)O(mk)
  • 空间复杂度:O(k)O(k)

总结

这题的关键不是算法技巧,而是把题面描述准确翻译成代码。

只要把“一次洗牌”的顺序处理对,再抓住“第 p 个玩家对应最终牌堆等差位置”这一点,就能稳定写对。