建议访问原文出处,获得更佳浏览体验。
原文出处:https://hyp1231.github.io/2018/09/01/20180901-2018nanjing-online-c/
题意
n n 个人玩纸牌游戏,牌堆里初始有 张牌。开始每人一次性从牌堆顶抓五张牌。大家依次出牌,直到有一人手中无牌,此人获胜。
游戏流程为:
- 牌的大小顺序为 [3,4,…,12,13,1,2] [ 3 , 4 , … , 12 , 13 , 1 , 2 ] ,从小到大。
- 第一个人出他手中最小的牌。
- 每个人要出上一张牌的下一个顺序的牌,如上一张出了 13 13 ,这张就要出 1 1 。
- 当上一张牌不是 的时候,可以出 2 2 当作任何牌。
- 一个人无法出牌,就跳过。
- 当一个人出了牌后,其他人都无法出牌,就从这个人开始一人抓一张牌;之后这个人出一张手里最小的牌。
- 当牌堆为空时,跳过抓牌操作。
输出游戏结束时,每个人手里的牌面额之和。如果这个人获胜,输出 Winner。
链接
题解
一道题意很复杂的大模拟。
首先规范一下数据储存的形式。开一个二维数组 cnt[i][j]
代表玩家 手中花色为
j
j
的牌的数量,以及 tot[i]
数据记录玩家 手中的牌数。之后每次有抓牌 / 出牌操作时,都注意两个数组同时修改。注意每次出牌后,都要判断该玩家手中是否有牌。
数组 stack[]
记录牌堆情况,top
为栈顶指针,当 top == m
时牌堆为空。
抽象出函数 int find_min(int id)
,输入玩家
id
i
d
,返回玩家手中最小的牌。
抽象函数 void draw(int id)
表示从玩家
id
i
d
开始,轮流抓一张牌。注意控制牌堆剩余数量,当牌堆为空时不进行抓牌操作。
在游戏主过程中,记录上一次出的牌,以及上一个出牌的人。值得注意的是,在下面三种情况下,都会出现轮圈抓牌的情况(draw
函数):记
next
n
e
x
t
表示恰好比上一张牌大的牌。
- 一圈无人出牌。
- next n e x t 为 2 2 。
- 手中没有 ,但还有 2 2 。
对于 这两种情况,由于出了一张 2 2 <script type="math/tex" id="MathJax-Element-43">2</script> 后,自然无人能管得上你的牌,可以直接跳过这一圈,进行抓牌、出当前最小牌的操作。
更多细节见下方代码实现。
代码
#include <cstdio>
#include <queue>
#include <vector>
#include <functional>
#include <cstring>
const int M = 20010;
const int N = 256;
int order[14] = { 0, 12, 13, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
// order[i] 表示花色 i 的排序位置
int list[14] = { 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1, 2 };
// list[i] 表示排第 i 的花色
int stack[M]; // 牌堆
int n, m, top; // top 是栈顶指针,当 top == m 时,牌堆为空
int cnt[N][14]; // cnt[i][j] 表示玩家 i 手中花色为 j 的牌的数量
int tot[N]; // tot[i] 表示玩家 i 手中的牌的数量
void init() {
int id = 0; top = 0;
memset(cnt, 0, sizeof(cnt));
memset(tot, 0, sizeof(tot));
while (top < m && id < n) {
++id;
for (int i = 0; i < 5 && top < m; ++i) {
int t = stack[++top];
++cnt[id][t];
++tot[id];
}
}
} // 初始化变量,及首轮摸牌
int find_min(int id) {
for (int i = 1; i <= 13; ++i) {
int u = list[i];
if (cnt[id][u] != 0) {
return u;
}
}
return -1;
} // 返回玩家 id 手中点数最小的牌,-1 表示空
void draw(int id) {
for (int i = 1; i <= n && top < m; ++i) {
int u = stack[++top];
++cnt[id][u];
++tot[id];
++id; if (id > n) { id = 1; }
}
} // 从玩家 id 开始顺时针抓牌一圈。注意当牌被摸完时跳出循环。
int play() {
int id = 1; // 玩家 id
int last; // 上一轮出的牌的花色
int last_player = 1;// 上一个出了牌的玩家 id
last = find_min(1); // 玩家 1 出手中最小的牌
--cnt[1][last];
--tot[1];
while (1) {
++id; // 下一位玩家
if (id > n) id = 1;
if (id == last_player) {
draw(id);
int next = find_min(id);
--cnt[id][next];
--tot[id];
if (tot[id] == 0) return id;
last = next;
last_player = id;
continue;
} // 说明一圈过后,无人出牌
if (last == 2) continue; // 上一张是 2,一定没有更大的牌出
int next = list[order[last] + 1]; // 找到恰好大 1 点的牌 next
if (cnt[id][next] > 0 && next != 2) { // 可以出 next 牌
--cnt[id][next];
--tot[id];
last = next;
last_player = id;
if (tot[id] == 0) return id;
} else if (cnt[id][2] > 0) { // 可以出一张 2
--cnt[id][2];
--tot[id];
if (tot[id] == 0) return id;
draw(id); // 抓一圈牌
int next = find_min(id);
--cnt[id][next];
--tot[id];
if (tot[id] == 0) return id;
last = next;
last_player = id;
}
}
return id;
} // 游戏主过程,返回胜利者
int main() {
int T;
scanf("%d", &T);
for (int Case = 1; Case <= T; ++Case) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
scanf("%d", &stack[i]);
} // 输入牌堆
init();
int winner = play();
printf("Case #%d:\n", Case);
for (int i = 1; i <= n; ++i) {
if (winner == i) {
printf("Winner\n");
} else {
int sum = 0;
for (int j = 1; j <= 13; ++j) {
sum += j * cnt[i][j];
}
printf("%d\n", sum);
}
}
}
return 0;
}