[语言月赛 202507] 卡牌游戏 的题解
在解题之前提交题解的代码会导致封禁。
作者:
概述
给定一副从顶到底为 的牌(是
的一个排列)与参数
。洗牌规则是:先按
把牌堆切成上堆 A(前
张)与下堆 B(后
张),再从 A 开始轮流取两堆顶部的牌追加到新牌堆的底部,直到有一堆被取空,最后把另一堆剩下的牌按原顺序整段接到新牌堆底部。全体操作没有任何随机性,本质是把原数组按下标重新排布,因此只需用两个下标在原数组上顺序扫描一遍即可,时间
。
分析
核心观察
整个过程是完全确定的:每一轮依次取 A 顶、B 顶各一张,先 A 后 B,写成序列就是 。
因此,输出牌的来源可以被拆成两段:
- 交错段:一共进行
轮,每轮写 2 张牌,故前
张牌恰好是 A、B 两堆从顶部开始的一一交错;
- 收尾段:轮数走到
时,短的那一堆恰好被取空,长的那一堆剩下的牌在各自数组里仍是一段连续区间,按原顺序整段接上即可。
关键点是:新牌堆只做“追加到末尾”这一种操作,所以完全不需要真的用队列去模拟“取顶 / 压底”——新牌堆的内容就是原数组按下标重排的结果,“取出的牌依次追加”与“按同样次序直接读取原数组”是同一件事。收尾段也可以直接照抄:若 ,B 的剩余就是下标
的连续一段;若
,A 的剩余就是下标
的连续一段。
两个退化情形可以直接看出来: 时 B 是空堆,一次都不取,输出与原牌堆相同;
时交错一轮后 A 即被取空,B 的剩余恰好是原来的第
张,输出同样与原牌堆相同。另外当
(
为偶数)时两堆等长,没有收尾段,输出是严格的完美交错。
思路
把数组下标当成两个游标: 指向 A 的顶部(初始
),
指向 B 的顶部(初始
,即输入的第
张牌)。只要两堆都还有牌,就交替写出
和
,然后两个游标同时后移一格;退出循环时至少有一堆已被取空(更准确地说,恰好是短的那一堆),把两个游标各自尚未走到头的部分按原顺序追加即可。
用公式描述,记 ,答案数组
为
其后:
- 若
(B 不短于 A):
,其中
,即下标
的牌原地不动;
- 若
(A 更长):
,其中
,即把 A 中还没取的牌依次接上。
下面两件事保证了游标做法的正确性:一是每轮取牌的先后顺序固定为 A、B,所以交错段的写法和公式一致;二是循环结束时不可能两堆同时还有牌(取空一轮就停),所以收尾时至多只有一段需要追加,且追加的牌本来就在原数组里连续存放、顺序不变,与题面“将仍剩余卡牌的牌堆的所有牌按原顺序放到新牌堆的底部”完全吻合。
顺带一提,也可以完全按题面字面用双端队列逐张模拟(见文末 Python 参考实现):每张牌只经历一次“取队首、压队尾”,单步代价 ,总复杂度同样是
,两种写法互为对拍。
具体示例
样例 1:,
,牌堆为
。此时 A 为
,B 为
,
,交错段为
与
共两轮;随后 A 已空,B 还剩
(下标
的牌,确实原地不动),整段接上得
。
样例 2:,
,牌堆为
。A 为
,B 为
,交错一轮得
;此时 A 被取空,B 的剩余
接上,结果为
,与原牌堆完全相同——正对应前面指出的
时洗牌是恒等变换。
样例 3:,
,牌堆为
。两堆等长:A 为
,B 为
,一共 6 轮、没有收尾段,输出是严格交错
。
算法步骤
- 读入
,再读入
个整数存入数组
(下标从 0 开始)。
- 置游标
(A 顶)与
(B 顶),答案数组
置空。
- 当
且
时循环:把
追加到
末尾,再把
追加到
末尾,然后令
、
。
- 循环结束后追加尚未取完的部分:只要
就依次追加
;只要
就依次追加
。这两段至多只有一段非空。
- 把
的
个元素按空格分隔成一行输出。
复杂度分析
- 时间:
。交错阶段共
轮、每轮写 2 张牌,收尾阶段把剩余牌各写一次,每张牌恰好写入
一次,单张牌的处理只含一次读取、一次写入和常数次下标自增。
- 空间:
,用于保存输入数组与答案数组。若改成原地重排可以压到
额外空间,但题面限制
,直接开数组更清晰,也没有必要优化。
- 即使用队列按题面逐张模拟,取队首与压队尾都是
的,总时间仍是
,这也是题面数据范围内完全可行的做法。
实现注意事项
- 循环条件要用“两个游标是否都还没到头”来判断,不要写成固定的
轮、
轮或
轮:轮数应当是
,写错会漏牌或多取牌。
- B 的队首在 0 下标数组里是
a[k](输入的第张牌);若误写成
a[k-1],会把 A 的最后一张牌重复取出一次。 - 收尾的两段不必先判断谁长谁短:循环退出时短的那堆必然已经取空(取空长度恰为
),因此“两段都无条件追加”的写法天然正确,也省去了分类讨论。
- “剩余牌堆整段接上”与“继续一张一张取下去”结果相同——因为整段接上只是把同一段牌按同样顺序追加,这一点本身就是循环退出条件的由来,不需要再单独模拟。
- 退化情形无需特判:
时循环体一次不执行,输出就是原牌堆;
时同样输出原牌堆;
时唯一合法的输入是
,输出那张牌本身。
- 卡牌编号是
的排列,值域很小,
int完全够用,也不存在需要去重的情况。 - 输出是一行
个空格分隔的整数,行末换行使输出更规范;多数判题器忽略行末空白,但不依赖这一点。
- C++ 里用
scanf/printf或cin/cout都可以,本题输入输出规模()远小于需要关同步的级别。
源代码
#include <cstdio>
#include <vector>
int main() {
int n, k;
if (scanf("%d %d", &n, &k) != 2) return 0;
std::vector<int> a(n);
for (int i = 0; i < n; ++i) scanf("%d", &a[i]);
// 分堆:A = a[0..k-1],B = a[k..n-1],两者都是「顶到底」的顺序。
// 从 A 开始轮流取各自顶部放到新牌堆底部;任一牌堆抽空后,把另一堆
// 剩下的牌按原顺序整段接上去——注意「整段接上」等价于「多取一张后
// 再接剩余」,因为多取的那张本来就是剩余段的队首。
std::vector<int> c;
c.reserve(n);
int i = 0, j = k;
while (i < k && j < n) {
c.push_back(a[i++]);
c.push_back(a[j++]);
}
while (i < k) c.push_back(a[i++]);
while (j < n) c.push_back(a[j++]);
for (int t = 0; t < n; ++t) printf("%d%c", c[t], t + 1 == n ? '\n' : ' ');
return 0;
}
import sys
from collections import deque
def main():
data = sys.stdin.read().split()
n = int(data[0])
k = int(data[1])
a = list(map(int, data[2:2 + n]))
# 按题面字面模拟:A = 前 k 张,B = 其余;从 A 开始轮流取各堆顶部,
# 追加到新牌堆底部;一堆空了就把另一堆剩下的整段接上。
pile_a = deque(a[:k])
pile_b = deque(a[k:])
new_pile = deque()
take_a = True
while pile_a and pile_b:
new_pile.append(pile_a.popleft() if take_a else pile_b.popleft())
take_a = not take_a
new_pile.extend(pile_a if pile_a else pile_b)
sys.stdout.write(" ".join(map(str, new_pile)) + "\n")
if __name__ == "__main__":
main()
评论