洛谷 P11227 [CSP-J 2024] 扑克牌
洛谷 P11227 [CSP-J 2024] 扑克牌
恩师:hnjzsyjyj
一、题目介绍:认识扑克牌问题
1.1 题目背景
扑克牌是一种广泛流传的娱乐工具,标准扑克牌通常包含 52 张牌,分为 4 种花色(黑桃、红桃、梅花、方块)和 13 种点数(A、2-10、J、Q、K)。本题以扑克牌为背景,考察对数据去重和计数的基本编程能力,是 2024 年 CSP-J(非专业级软件能力认证)的入门级题目。
1.2 题目描述
题目可以简化为:
- 标准扑克牌有 52 张不同的牌
- 输入 n 张扑克牌(可能有重复)
- 计算这些牌中不同牌的数量(去重后的数量)
- 最终输出 52 减去这个数量,即 "还缺少多少种不同的牌"
简单来说,就是要找出输入的牌中有多少种是独特的,然后计算完整的 52 张牌中还缺少多少种。
1.3 输入输出格式
- 输入:第一行是一个整数 n(表示输入的牌的数量),接下来 n 行每行是一张扑克牌的表示(如 "SA" 表示黑桃 A,"HJ" 表示红桃 J 等)
- 输出:一个整数,表示 52 张标准牌中缺少的牌的种类数
1.4 示例说明
以示例输入为例:
plaintext
5
SA
SA
HK
DQ
CQ
- 输入了 5 张牌,但其中 "SA" 出现了 2 次
- 去重后不同的牌有 4 种:SA、HK、DQ、CQ
- 因此缺少的牌的种类数为 52-4=48
- 输出结果:
48
二、解题思路:用映射表实现去重计数
2.1 核心问题分析
本题的核心是解决两个问题:
- 如何判断一张牌是否已经出现过(去重)
- 如何统计不同牌的总数
这两个问题可以通过映射表(map) 数据结构完美解决,因为 map 可以存储键值对,并且键是唯一的,非常适合用于去重和计数场景。
2.2 解题步骤
- 读取输入的牌的数量 n
- 创建一个映射表(map)用于存储出现过的牌
- 循环读取 n 张牌:
- 对于每张牌,检查它是否已经在映射表中
- 如果不在,就将它加入映射表,并更新不同牌的计数
- 计算 52 减去不同牌的总数,得到缺少的牌的种类数
- 输出结果
2.3 为什么用 map?
在 C++ 中,map 是一种关联容器,它的特点是:
- 存储的元素是键值对(key-value)
- 键(key)是唯一的,不会重复
- 可以快速判断一个键是否存在(平均时间复杂度为 O (log n))
这些特性正好符合本题的需求:我们需要记录哪些牌已经出现过,并且统计不同牌的数量。使用 map 可以让代码更简洁、高效。
三、代码解析:逐行理解程序逻辑
下面我们来详细分析用户提供的代码,看看它是如何实现上述解题思路的:
cpp
运行
#include<bits/stdc++.h>
using namespace std;
map<string,int> m;
string a;
int n,ans;
int main() {cin>>n;while(n--){cin>>a;if(!m[a])m[a]=++ans;}cout<<52-ans;return 0;
}
3.1 头文件与命名空间
cpp
运行
#include<bits/stdc++.h>
using namespace std;
#include<bits/stdc++.h>
:这是 C++ 的万能头文件,包含了所有标准库,包括我们需要的 map 容器using namespace std;
:使用标准命名空间,这样我们可以直接使用map
、cin
、cout
等,而不必添加std::
前缀
3.2 全局变量定义
cpp
运行
map<string,int> m;
string a;
int n,ans;
定义了 4 个全局变量:
map<string,int> m
:一个映射表,键是 string 类型(存储牌的表示),值是 int 类型(存储该牌首次出现的序号)string a
:用于临时存储当前读取的牌int n
:存储输入的牌的数量int ans
:用于统计不同牌的数量,初始值为 0(全局变量默认初始值)
3.3 主函数入口
cpp
运行
int main() {// 程序逻辑return 0;
}
main()
函数是程序的入口,所有代码都在其中执行。
3.4 读取牌的数量
cpp
运行
cin>>n;
通过cin
读取输入的第一个整数 n,即牌的总数量。
3.5 循环读取每张牌
cpp
运行
while(n--){// 处理每张牌的逻辑
}
这是一个while
循环,会执行 n 次(每次循环后 n 的值减 1,直到 n 变为 0),用于读取 n 张牌并处理。
3.6 处理单张牌
cpp
运行
cin>>a;
if(!m[a])m[a]=++ans;
这两行是程序的核心逻辑:
cin>>a
:读取一张牌,存储到字符串变量 a 中if(!m[a])m[a]=++ans
:m[a]
:访问 map 中键为 a 的元素的值- 如果该牌之前没有出现过(
m[a]
的值为 0),则!m[a]
为真 - 此时执行
m[a]=++ans
:将该牌加入 map,并将 ans 的值加 1 后赋给它
这个逻辑的巧妙之处在于:只有当牌第一次出现时,才会更新 ans 的值,从而实现了去重计数的功能。
3.7 输出结果
cpp
运行
cout<<52-ans;
循环结束后,ans 的值就是不同牌的数量。输出 52 减去 ans,即缺少的牌的种类数。
四、代码执行过程演示
为了更直观地理解代码的执行过程,我们以示例输入为例,分步演示程序的运行:
示例输入:
plaintext
5
SA
SA
HK
DQ
CQ
初始状态:m 为空,ans=0,n=5
第一次循环(n=5→4):
- 读取 a="SA"
- 检查 m ["SA"],此时 map 中没有 "SA",m ["SA"]=0
- 执行 m ["SA"]=++ans:ans 变为 1,map 中添加 "SA"→1
- 当前 map:{"SA":1},ans=1
第二次循环(n=4→3):
- 读取 a="SA"
- 检查 m ["SA"]=1(非 0)
- 不执行任何操作
- 当前 map:{"SA":1},ans=1
第三次循环(n=3→2):
- 读取 a="HK"
- 检查 m ["HK"]=0
- 执行 m ["HK"]=++ans:ans 变为 2,map 中添加 "HK"→2
- 当前 map:{"SA":1, "HK":2},ans=2
第四次循环(n=2→1):
- 读取 a="DQ"
- 检查 m ["DQ"]=0
- 执行 m ["DQ"]=++ans:ans 变为 3,map 中添加 "DQ"→3
- 当前 map:{"SA":1, "HK":2, "DQ":3},ans=3
第五次循环(n=1→0):
- 读取 a="CQ"
- 检查 m ["CQ"]=0
- 执行 m ["CQ"]=++ans:ans 变为 4,map 中添加 "CQ"→4
- 当前 map:{"SA":1, "HK":2, "DQ":3, "CQ":4},ans=4
循环结束:输出 52-4=48,与预期结果一致
通过这个过程可以清晰地看到,代码如何通过 map 实现了去重,并正确统计了不同牌的数量。
五、map 容器的工作原理
5.1 map 的基本概念
map 是 C++ 标准库中的一种关联容器,它按照键(key)的顺序存储键值对(key-value pairs)。map 中的键是唯一的,这意味着不能有两个元素拥有相同的键。
在本题中,我们使用 map<string, int>,表示键是字符串类型(存储牌的标识),值是整数类型(存储该牌首次出现的序号)。
5.2 为什么 m [a] 初始值为 0?
在 C++ 中,当我们访问 map 中不存在的键时,map 会自动插入该键,并将其对应的值初始化为该类型的默认值:
- 对于 int 类型,默认值是 0
- 对于 string 类型,默认值是空字符串
这就是为什么当我们第一次访问 m ["SA"] 时,它的值是 0,我们可以通过if(!m[a])
来判断该牌是否是第一次出现。
5.3 ++ans 的作用
m[a] = ++ans
这个操作有两个作用:
++ans
:将计数器 ans 的值加 1,统计新出现的牌m[a] = ...
:将该牌添加到 map 中,并赋值为当前 ans 的值(表示这是第几个出现的新牌)
这样,当再次遇到相同的牌时,m [a] 的值已经不是 0 了,不会再被计数。
5.4 为什么不用数组?
可能有同学会问:为什么不用数组来存储牌的出现情况?主要有两个原因:
- 牌的标识是字符串(如 "SA"、"HK"),而数组的下标只能是整数
- 牌的种类是固定的 52 种,但用数组需要建立字符串到整数的映射,反而更麻烦
使用 map 可以直接用牌的字符串作为键,更加直观和方便。
六、易错点分析:这些坑要注意
6.1 对 map 的访问会自动插入元素
初学者容易忽略的一点是:在 map 中访问一个不存在的键时,会自动插入该键并赋予默认值。例如:
cpp
运行
map<string, int> m;
if (m["SA"] == 0) {// 即使原本没有"SA",这里也会插入"SA"并赋值0
}
在本题的代码中,这个特性恰好被合理利用,但在其他场景下可能导致意外的结果。
6.2 全局变量与局部变量的区别
示例代码中将 ans 定义为全局变量,默认初始值为 0。如果将 ans 定义为 main 函数内的局部变量,必须显式初始化:
cpp
运行
int main() {int ans = 0; // 必须显式初始化,否则值是不确定的// ...
}
否则 ans 的初始值是随机的,会导致统计结果错误。
6.3 输入输出格式错误
虽然本题的输入输出格式简单,但仍需注意:
- 第一行是 n,之后 n 行每行是一张牌
- 输出是一个整数,不需要其他文字
6.4 字符串比较的问题
在 C++ 中,字符串的比较是按字典顺序进行的,这正好符合我们的需求。例如 "SA" 和 "SA" 会被判定为相等,而 "SA" 和 "SB" 会被判定为不相等。
但需要注意输入的牌的格式是否统一,例如是否区分大小写(题目中通常会说明牌的表示方法)。
6.5 边界情况处理
需要注意一些边界情况:
- n=0:输入 0 张牌,此时输出 52
- n=52 且所有牌都不同:输出 0
- n>52:可能有很多重复的牌,只需统计不同的数量
示例代码已经正确处理了这些边界情况,因为:
- 当 n=0 时,循环不执行,ans=0,输出 52-0=52
- 当所有牌都不同时,ans=52,输出 52-52=0
七、代码优化与拓展
7.1 使用 unordered_map 提高效率
在 C++ 中,map 的底层实现是红黑树,查找元素的时间复杂度是 O (log n)。如果 n 非常大,可以使用 unordered_map,它的底层实现是哈希表,查找元素的平均时间复杂度是 O (1):
cpp
运行
#include<bits/stdc++.h>
using namespace std;
unordered_map<string,int> m; // 改为unordered_map
string a;
int n,ans;
int main() {cin>>n;while(n--){cin>>a;if(!m[a])m[a]=++ans;}cout<<52-ans;return 0;
}
对于本题规模的输入,两者效率差异不大,但了解这种优化方法有助于解决更大规模的问题。
7.2 使用 set 实现去重
除了 map,我们还可以使用 set(集合)来实现去重功能,因为 set 中也不能有重复元素:
cpp
运行
#include<bits/stdc++.h>
using namespace std;
set<string> s;
string a;
int n;
int main() {cin>>n;while(n--){cin>>a;s.insert(a); // insert方法会自动忽略重复元素}cout<<52-s.size(); // size()返回集合中元素的数量return 0;
}
这种方法代码更简洁,直接利用 set 的特性实现去重,然后通过 size () 方法获取不同元素的数量。
7.3 手动实现哈希表(拓展学习)
对于学习深入的同学,可以尝试手动实现一个简单的哈希表来解决这个问题,这有助于理解 map 和 set 的底层原理。
基本思路是:
- 创建一个足够大的数组
- 设计一个哈希函数,将牌的字符串转换为数组下标
- 使用开链法或线性探测法处理哈希冲突
- 记录哪些下标被使用过,统计总数
这是一个很好的练习,可以加深对数据结构的理解。
八、知识点总结:从扑克牌问题学到的编程思想
8.1 去重计数的通用方法
本题本质上是一个去重计数问题,这类问题的通用解决方法有:
- 使用集合(set)存储元素,自动去重,然后获取集合大小
- 使用映射表(map)存储元素是否出现,通过判断值来计数
- 先排序再遍历,通过比较相邻元素来计数
在 C++ 中,使用 set 或 map 是最简洁高效的方法。
8.2 关联容器的应用
本题展示了关联容器(map)的典型应用场景:
- 需要判断元素是否存在
- 需要统计不同元素的数量
- 需要根据键快速查找
掌握关联容器的使用是 C++ 编程的重要基础,在很多实际问题中都有应用。
8.3 问题简化的思维
解决编程问题时,经常需要将复杂问题简化:
- 本题将 "计算缺少的牌的种类数" 简化为 "52 减去已出现的不同牌的数量"
- 而 "已出现的不同牌的数量" 又可以通过去重计数来解决
这种化繁为简的思维方式是解决复杂问题的关键。
8.4 细节处理的重要性
虽然本题代码简短,但包含了很多细节处理:
- 全局变量的默认初始化
- map 访问不存在的键时的自动插入特性
- 前缀 ++ 运算符的使用时机
这些细节处理体现了编程的严谨性,也是保证程序正确性的关键。
九、PPT 展示建议
如果用这篇内容制作 PPT,可以按照以下结构组织,突出重点,方便讲解:
封面页:
- 标题:洛谷 P11227 [CSP-J 2024] 扑克牌
- 副标题:使用 map 实现去重计数
- 背景图:扑克牌相关图片
题目介绍页:
- 简洁描述题目要求
- 列出输入输出格式
- 展示 1-2 个示例(用表格形式展示输入和输出)
核心问题分析页:
- 提炼问题本质:去重计数
- 说明为什么需要去重
- 展示最终计算方式:52 - 不同牌的数量
解题思路页:
- 用流程图展示解题步骤
- 突出 map 的作用
- 说明每一步的操作
代码框架页:
- 展示完整代码
- 用不同颜色标注关键部分
- 简要说明代码结构
代码解析页(分 2-3 页):
- 逐行解释代码含义
- 重点讲解 map 的使用和条件判断
- 说明 ans 变量的作用
执行过程演示页:
- 以示例输入为例
- 分步展示循环执行过程
- 用图示展示 map 的变化和 ans 的取值
map 工作原理页:
- 简单介绍 map 的特性
- 解释为什么 m [a] 初始值为 0
- 展示 map 如何实现去重
易错点与优化页:
- 列出常见错误类型
- 展示优化方法(如使用 set)
- 对比不同方法的优缺点
总结与拓展页:
- 提炼核心知识点
- 推荐相关练习题目
- 总结去重计数问题的解决方法
十、写在最后
扑克牌问题虽然是一道简单的入门级编程题,但它很好地展示了关联容器在实际问题中的应用。通过解决这道题,我们不仅学会了具体的解题方法,更重要的是掌握了去重计数这一常见问题的解决思路,理解了 map 等关联容器的工作原理。
在编程学习中,这类看似简单的题目往往蕴含着重要的基础知识和思维方法。它们是构建更复杂程序的基石,值得我们深入理解和掌握。
记住,优秀的程序员不仅能写出正确的代码,更能理解代码背后的原理,知道为什么这样写,以及有没有更好的写法。通过不断思考和优化,我们的编程能力才能不断提升。
希望这篇博客能帮助你更好地理解扑克牌问题和关联容器的应用。如果有任何疑问或建议,欢迎在评论区留言讨论!