news 2026/11/6 3:38:26

CSP-J 2026 第二轮入门组真题解析(第四题 、纸牌游戏(card))

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CSP-J 2026 第二轮入门组真题解析(第四题 、纸牌游戏(card))



CSP-J 2026 入门组第四题:《纸牌游戏(card)》

这道题很有意思!它表面上是在玩纸牌,实际上考查的是 栈的思想、逆向思考,以及如何把重复的操作优化成线性时间。

前三题我们接触了字符串、位运算和贪心算法。第四题则要带大家认识一种新的思考方式:

当正着做很多遍太慢时,能不能倒过来做一次?

我们今天就跟着小 Z 一起玩一场纸牌魔法游戏!


一、先读懂游戏规则

想象小 Z 面前有一张长桌子,桌面上的纸牌从左向右排成一行。

每张牌上有一个点数,例如:

1、2、3、4

每次打出一张新牌时,有两种情况:

  • 如果桌面上没有这种点数,就把新牌放到最右边。

  • 如果桌面上已经有这种点数,就把它以及它右边的所有牌全部收走,再把新牌放到最右边。

注意:被收走的牌,本局游戏中就不能再使用了。

举个例子

桌面原来有:


接下来又打出一张点数为 4 的牌。

桌面原来是:

1 4 2 3

因为已经有一张 4,所以要把 4 以及它右边的 2、3 全部收走,再放入新的 4。

这就是游戏的基本规则。


二、题目到底让我们求什么?

题目给出 n 张牌,点数依次为:

a1, a2, ..., an

小 Z 要进行 n 局游戏。

  • 第 1 局:打出第 1 张到第 n 张。

  • 第 2 局:跳过第 1 张,打出第 2 张到第 n 张。

  • 第 3 局:跳过前 2 张,打出第 3 张到第 n 张。

  • ……

  • 第 n 局:只打出最后一张。

也就是说,我们要分别处理这些后缀:


最后要统计:

  • 有多少局结束时桌面上有点数 1?

  • 有多少局结束时桌面上有点数 2?

  • 有多少局结束时桌面上有点数 3?

注意:统计的是有多少局包含这种点数,不是这种点数的牌一共出现了多少次。


三、先用样例玩一遍

题目样例 1:

4 3 1 2 1 3

意思是:

  • 一共有 4 张牌;

  • 点数种类有 3 种;

  • 牌的顺序是1 2 1 3。


我们先不考虑优化,看看每一局最后会怎样。

第 1 局:打出 1、2、1、3

操作

桌面

打出 1

1

打出 2

1 2

再打出 1

1

打出 3

1 3

最后是1 3。


第 2 局:打出 2、1、3

操作

桌面

打出 2

2

打出 1

2 1

打出 3

2 1 3

最后是2 1 3。


第 3 局:打出 1、3

最后是1 3。


第 4 局:只打出 3

最后是3。

因此:

点数

出现在最终桌面的局数

1

3

2

1

3

4

答案是:

3 1 4

到这里,孩子应该已经理解题意了。

但问题来了:如果我们真的把每一局都模拟一遍,会不会太慢?


四、为什么不能直接模拟 n 局?

题目给出的数据范围是:

  • 2 ≤ n ≤ 5 × 10^5,1 ≤ m ≤ 3 × 10^5;

最多有 50 万张牌,也就有 50 万局游戏。

如果每一局都重新模拟,最坏情况下可能要处理:

n+(n-1)+(n-2)+.......+1

这相当于:

当 n 达到 50 万时,这个操作量非常大,计算机很难在规定时间内完成。

所以,我们要想办法:

不要玩 50 万遍游戏,而是想办法只从头到尾处理一次牌。

这就是本题最重要的突破口。


五、真正的突破:从第一张牌开始,找到它最后一次出现的位置

现在我们换一种思考方式。

假设一局游戏的第一张牌是点数 1。

在这张 1 后面,可能出现很多其他点数。但是在最后一次出现 1 之前,桌面上的 1 始终不会被其他点数收走,因为其他点数只能收走自己以及右边的牌,不能收走左边的 1。

当 1 最后一次出现时,它会把自己右边的所有牌收走。

所以,在最后一次出现 1 之后,桌面一定可以看成:

1 + 后面剩余牌形成的最终桌面

这就是本题的关键!


1. 用样例理解这个规律

牌的顺序:

1 2 1 3

我们先看第一张牌 1。


所以,第 1 局最终桌面一定是:

1 + 第 4 张牌之后形成的桌面

第 4 张牌是 3,而且它后面没有牌了。

因此最终桌面是:

1 3


我们再看第 2 局:

2 1 3

第一张牌是 2,它最后一次出现的位置就是第 2 张牌。

因此:

2 + 第 3 张牌之后形成的桌面

第 3 张牌是 1,最后一次出现的位置就是第 3 张。

再接着看第 4 张牌 3。

最终桌面:

2 1 3

是不是很神奇?

我们不需要模拟每一张牌怎么被收走,只需要不断找到当前点数最后一次出现的位置!


2. 把这个规律变成算法

我们定义一个数组:

last[x]

表示点数 x 在整叠牌中最后一次出现的位置。

例如:

位置: 1 2 3 4 点数: 1 2 1 3

那么:

点数 x

last[x]

1

3

2

2

3

4


接下来定义:

nxt[i] = last[a[i]] + 1;

它表示:

如果从第 i 张牌开始玩,第一张牌的最后一次出现位置之后,下一次应该从哪里继续寻找。

对于样例:

a: 1 2 1 3 位置: 1 2 3 4

得到:

起点 i

a[i]

last[a[i]]

nxt[i]

1

1

3

4

2

2

2

3

3

1

3

4

4

3

4

5

这里的 5 表示已经超过最后一张牌,游戏结束。

于是,我们得到几条路线:

这条路线上的每个数字,就是最终桌面上留下的点数。


六、第二个关键:一条路线可能被很多局共同使用

现在我们已经知道了每一局最终桌面上的数字是怎样找到的。

但还有一个问题:

题目要求统计每个点数出现在多少局中。

如果每一局都沿着路线走一遍,仍然可能很慢。

所以我们再想一步:

如果很多局都会经过同一个位置,能不能把它们合并统计?

答案是可以的。


我们定义:

ways[i]

表示有多少局游戏的寻找路线会经过第 i 个位置。

举个例子:

位置: 1 2 3 4 点数: 1 2 1 3

前面我们算出了:

nxt[1] = 4 nxt[2] = 3 nxt[3] = 4 nxt[4] = 5

每一局都从自己的起点开始,所以每个位置最初都有 1 条路线经过它。

因此:

ways[i] = 1;

但是路线还会继续向后走。

例如:

  • 第 1 个位置的路线会走到第 4 个位置;

  • 第 2 个位置的路线会走到第 3 个位置;

  • 第 3 个位置的路线也会走到第 4 个位置。

我们把这些路线数量累加起来。


1. 路线数量如何传递?

核心代码只有两行:

ways[nxt[i]] += ways[i];

意思是:

第 i 个位置有多少条路线经过,就把这些路线全部传递给它的下一个位置。

我们从左到右处理,因为nxt[i]一定大于 i。


2. 手工推演 ways 数组

初始:

位置: 1 2 3 4 ways: 1 1 1 1

处理第 1 个位置:

nxt[1] = 4

把第 1 个位置的 1 条路线传给第 4 个位置:

ways:1 1 1 2

处理第 2 个位置:

nxt[2] = 3

把第 2 个位置的 1 条路线传给第 3 个位置:

ways:1 1 2 2

处理第 3 个位置:

nxt[3] = 4

第 3 个位置现在有 2 条路线经过,所以把 2 加到第 4 个位置:

ways:1 1 2 4

处理第 4 个位置:

它的下一位置是 5,已经超出范围,不需要再传递。

最终得到:

位置

点数

ways[i]

1

1

1

2

2

1

3

1

2

4

3

4


这意味着:

  • 第 1 个位置有 1 局会经过;

  • 第 2 个位置有 1 局会经过;

  • 第 3 个位置有 2 局会经过;

  • 第 4 个位置有 4 局会经过。


七、最后一步:统计每种点数出现的局数

现在就非常简单了!

每个位置上的点数是a[i],而ways[i]表示有多少局会经过这个位置。

因此:

ans[a[i]] += ways[i];

把经过这个位置的路线数量,累加到对应点数的答案中。


对于样例:

位置

点数

ways[i]

累加到

1

1

1

ans[1] += 1

2

2

1

ans[2] += 1

3

1

2

ans[1] += 2

4

3

4

ans[3] += 4

最终:

ans[1] = 3 ans[2] = 1 ans[3] = 4

输出:

3 1 4

样例答案正确


这里有一个重要细节:为什么可以直接把ways[i]加到ans[a[i]]?

因为一条路线不会经过同一种点数的两个位置。

当路线经过点数 x 的位置 i 时,它的下一位置是last[x]+1,已经跳过了 x 在后面所有出现的位置。因此,每一局的路线中,每种点数最多出现一次,不会重复计数。


八、完整 C++ 参考程序

现在把前面学到的内容组合起来。

#include <iostream> #include <vector> using namespace std; int main() { int n, m; cin >> n >> m; vector<int> a(n + 1); vector<int> last(m + 1, 0); vector<int> nxt(n + 1); vector<long long> ways(n + 1, 1); vector<long long> ans(m + 1, 0); // 第一步:读入牌,并记录每种点数最后出现的位置 for (int i = 1; i <= n; i++) { cin >> a[i]; last[a[i]] = i; } // 第二步:计算每个位置的下一个位置 for (int i = 1; i <= n; i++) { nxt[i] = last[a[i]] + 1; } // 第三步:从左到右传递路线数量 for (int i = 1; i <= n; i++) { if (nxt[i] <= n) { ways[nxt[i]] += ways[i]; } } // 第四步:统计每种点数出现的局数 for (int i = 1; i <= n; i++) { ans[a[i]] += ways[i]; } // 第五步:输出答案 for (int i = 1; i <= m; i++) { cout << ans[i]; if (i < m) cout << " "; } cout << endl; return 0; }

九、把整个算法整理成五个步骤

我建议记住下面这张流程图:

第一步:记录最后出现位置

last[x] = 点数 x 最后出现的位置

第二步:计算下一个位置

nxt[i] = last[a[i]] + 1

第三步:传递路线数量

ways[nxt[i]] += ways[i]

第四步:统计每种点数

ans[a[i]] += ways[i]

第五步:输出答案


十、复杂度分析:为什么它能通过考试?

我们看看程序做了多少工作。

操作

时间复杂度

读入数据,记录 last

O(n)

计算 nxt

O(n)

传递 ways

O(n)

统计答案

O(n)

输出答案

O(m)

总时间复杂度:

O(n+m)

空间复杂度:

O(n+m)

题目最大 n 为 50 万,m 为 30 万,这样的算法是可以高效处理的。


十一、最容易犯的四个错误

错误 1:把每一局都重新模拟一遍。

这样最坏需要 O(n^2) 的时间,会超时。


错误 2:把last[a[i]]当作下一个位置。

不对!我们要跳过最后一次出现的位置,所以必须加 1:

nxt[i] = last[a[i]] + 1;

错误 3:从右向左传递 ways。

本题的下一个位置一定大于当前位置,因此要从左向右传递:

for (int i = 1; i <= n; i++)

错误 4:只统计每个位置一次。

ways[i]不是固定的 1,它代表有多少局会经过这个位置。必须使用:

ans[a[i]] += ways[i];

而不是简单地ans[a[i]]++。


十二、举一反三:让孩子真正掌握这道题

这道题真正值得学习的,不只是纸牌游戏,而是三个算法思想。

  1. 最后一次出现位置:遇到重复元素时,思考它最后一次出现的位置有什么作用。

  2. 跳跃式处理:不必一个位置一个位置地走,可以直接跳到关键位置的后面。

  3. 路线数量合并:很多起点会走到同一个位置,可以把经过这个位置的路线数量累加起来。


给孩子留两道小练习。

练习 1:魔法数字牌

输入:

5 3 1 2 3 2 1

请完成:

  1. 求出last[1]、last[2]、last[3]。

  2. 求出每个位置的nxt[i]。

  3. 计算ways[i]。

  4. 最后求出三个点数的答案。


练习 2:自己设计一个小纸牌游戏

输入:

6 3 1 2 1 3 2 3

请尝试使用程序求解,并且用纸笔验证其中第 1 局和第 3 局的最终桌面。


这道题看起来是在考纸牌模拟,其实它考的是学生能不能从一大堆重复操作中,找到真正有用的信息。

先找到每种点数最后出现的位置,再建立位置之间的跳跃关系,最后把经过每个位置的路线数量合并起来。

这是一种很值得培养的算法思维:

不要重复做相同的事情,而要找到可以共享的计算结果。


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/6 3:38:17

华为校招机考真题 AI方向 2026-10-23 选择题【真题及答案解析】

华为校招&实习机试在线OJ刷题系统 &#xff5c; 在线OJ注册使用教程 1、自注意力层的输入序列包含 m 个 token&#xff0c;每个 token 表示成 k 维向量&#xff08;忽略 batch 维与多头&#xff09;。计算注意力得分矩阵 QKT 的复杂度是&#xff1f; A. O(m2 k) B. O(m …

作者头像 李华
网站建设 2026/11/6 3:37:27

大型建筑中央空调:螺杆机、离心机、多联机的适用场景与区别

螺杆机、离心机同属水系统冷水机组&#xff08;产出冷冻水&#xff0c;通过水管输送到各房间末端&#xff09;&#xff0c;是大型建筑中央空调的主流主机&#xff1b;多联机&#xff08;VRV/VRF&#xff09;属于氟系统&#xff0c;通过冷媒管路直接连接室内机&#xff0c;更侧重…

作者头像 李华
网站建设 2026/11/6 3:37:15

springboot社区化车辆流通服务平台28044-计算机毕业设计、课程设计

前言 ✨ 博主介绍&#xff1a;一线全栈工程师&#xff0c;毕设实战引路人。技术栈覆盖Java、Python、C#、PHP、Node.js及UniApp跨端开发&#xff0c;擅长多语言项目落地与架构设计。持续分享毕设源码、开题报告、技术选型心得与职场踩坑经验。用工程化思维写代码&#xff0c;帮…

作者头像 李华
网站建设 2026/11/6 3:37:11

为什么有的纸箱订单要收制版费?制版费和刀模费是一回事吗?

为什么有的纸箱订单要收制版费&#xff1f;制版费和刀模费是一回事吗&#xff1f;一、两种费用的常见混淆纸箱报价中常出现制版费和刀模费两项费用&#xff0c;很多采购者容易混淆&#xff0c;以为是同一笔费用重复收取。实际上二者完全不同&#xff0c;分别对应生产中的两个不…

作者头像 李华
网站建设 2026/11/6 3:37:05

DeepSeek LeetCode 525. 连续数组 Java实现

import java.util.HashMap; import java.util.Map;class Solution {public int findMaxLength(int[] nums) {// 前缀和&#xff08;把 0 当作 -1&#xff0c;1 当作 1&#xff09; -> 第一次出现该前缀和的下标Map<Integer, Integer> map new HashMap<>();// 前…

作者头像 李华