PTA第六章组合数据类型:字典与集合AC攻略 1. 第六章到底在考什么我印象里浙大版《Python 程序设计》的第六章主题是组合数据类型重点就是字典和集合。很多同学在PTA上一看到最高频的题目类型就是给你一堆数据让你统计出现次数、按频率排序、去除重复或者让你判断某个键存不存在。听起来很简单但真正在评测机上跑起来一堆人卡在超时、格式错误、改字典报错这类奇奇怪怪的地方。这一章的意义在于从单变量思维过渡到批量数据思维。前几章你还在写a b、写循环遍历列表到第六章你要开始回答“这一堆数据里谁出现得最多”“两个集合之间差了什么”这类问题。如果你还是只用列表遍历去实现很多题在PTA上会直接TLE超时。因为评测机对大数据量很敏感它的时间限制不会因为你“思路是对的”就网开一面。所以要过第六章你不只是要记住dict.get()怎么用还得知道什么时候该用集合、什么时候用字典、什么时候用默认字典defaultdict以及列表、字典、集合之间怎么互相转换。这些东西学完你后面刷题的速度会提上来一大截因为很多题本质就是“查字典”和“去重排序”的变体。1.1 理解组合数据类型的三种容器第六章里的题绕不开三个核心容器字典、集合、元组。它们各管一摊字典负责“键值映射”适合做统计、索引、缓存。集合负责“有无关系”适合做去重、交集、差集。元组负责“固定打包”适合做不可变的多字段记录比如(姓名, 分数)但在PTA题目里元组更多是作为字典的键值使用。做题前我习惯先问自己一个问题题目需要“存一个值”还是“记录一组值”还是“快速判断存在性”如果是记录单一个值的状态用字典最方便如果只是判断哪些数据出现过用集合最省事如果一个对象有多个属性且不允许修改元组是最稳的载体。这里要特别提醒千万不要一上来就用列表硬存。列表的查找是线性扫描300个数据没事300万个数据就直接击穿时间限制。我第一次刷第六章的时候统计词频用了列表一个个数小例子能过提交后一大片的运行超时后来换成字典瞬间AC。转变这个习惯比背十个函数都有用。1.2 为什么字典和集合总在一道题里出现第六章很多题目的典型模式是先读入一堆数据用字典统计最后对统计结果排序输出。也有的题是先对两个数据源做去重合并然后判断公共部分。这类题之所以高频是因为它们对应了真实编程里的数据清洗场景——比如日志分析、用户标签合并、交易记录汇总。这类题的核心难点其实只有一个如何正确组织键值关系。我见过很多同学代码写得没问题但AC不掉问题常出在“键的类型没统一”、“读入时空格没处理干净”、“排序的规则没写对”。所以我在下面写的解答思路全都是按照PTA上常见测试点的出题规律来拆的正常输入、大数量输入、边界输入、格式敏感输出。2. 我整理的AC解答通用思路在刷完第六章所有基础题之后我把题目分成了三类统计类、去重类、转换类。这个分类不是我随便定的而是根据解题模板不同来区分的。把题目分类之后做题效率高很多因为你不需要每道题都从头想数据结构直接套对应模板再改改键名和输出格式就行。2.1 统计类题目的通用三步模板统计类是第六章的重头戏什么“统计选课人数”“统计词频”“统计数字出现次数”都属于这一类。我的标准解法分三步初始化字典或默认字典。遍历输入数据对每个数据做dic[key] dic.get(key, 0) 1或使用defaultdict(int)。按题目要求排序和格式化输出。第一版代码往往会写出这种风格d {} for item in data: if item not in d: d[item] 1 else: d[item] 1这种写法没错但性能略差代码偏长。我更推荐用get的写法d {} for item in data: d[item] d.get(item, 0) 1原因很简单它把“判断存在 取值 加一 赋值”压缩成了一行而且避免了item not in d的两次哈希查找。实际跑起来差距虽然不大但代码更简洁对评测机也更友好。如果统计时需要默认值为列表那么一定要用defaultdict(list)。比如要记录每个班级有哪些学生普通的dict需要你先判断键存在再append容易漏写分支。换成defaultdict(list)后直接d[cls].append(name)少写一行更稳。2.2 去重与排序的基本功集合与列表转换第六章里去重题很多。PTA上的出题风格特别喜欢给你一个含重复元素的序列让你输出所有不同元素或者按指定顺序输出。最直接的方式就是用集合set因为集合本身就是“唯一化”的天然工具。但要注意一个高频坑集合是无序的可PTA经常要求“按输入顺序输出”或“按字典序输出”。这时候不能直接遍历集合。我见过太多同学在这里栽跟头——先转集合然后直接 for 循环遍历发现输出顺序和样例不一样就开始怀疑是不是评测机有问题。其实只要题目要求“按照输入顺序去重输出”你需要用另一种方法遍历原列表用一个集合记录已经出现过的元素只把第一次出现的元素收集起来seen set() result [] for x in data: if x not in seen: seen.add(x) result.append(x)这么做的原理是集合负责“记忆”列表负责“顺序”。如果你用set(data)再排序那是“按大小排序”不是“保持原序”两者完全不同。如果题目要求返回所有不重复元素并按字典序输出那么可以先sorted(set(data))这样又简洁又稳。其实这类题真正考的是你能否分清“排序规则”和“去重规则”是两件事。2.3 遍历字典的格式选择字典遍历在第六章的输出环节非常重要。你得知道三种遍历方式并清楚它们分别适用于什么场景for key in d:—— 遍历键适合只需要键的情况。for key, value in d.items():—— 同时遍历键值最常用。for value in d.values():—— 只遍历值适合只统计数据的情况。PTA输出时经常会要求先按键的某种规则排序再输出键和值。这时候实际用的代码是这样的for key in sorted(d): print(key, d[key])注意sorted(d)默认按键排序如果你需要按值排序就必须用items()配合lambdasorted(d.items(), keylambda x: x[1], reverseTrue)这种写法在词频TopN题目里几乎是标准答案。但还有一个细节sorted(d.items(), ...)返回的是元组列表你输出每一项时可以直接解包for k, v in sorted(d.items(), keylambda x: x[1], reverseTrue): print(f{k}: {v})这里面的lambda x: x[1]的意思是“取元组第二个元素作为排序依据”。很多新手写成keylambda x: x[0]结果按键排了序全错。我在教一个同学的时候他不理解为什么输出顺序不对我说你把x[1]改成分数,马上AC他改完直呼“原来 x 是元组”。3. 手把手过几道有代表性的AC题目光讲理论不够我挑了三个不同层次的题目把我自己的AC代码和思考过程写下来。这几道题我都在PTA上实际提交过直接作为参考模板是没问题的。3.1 案例一统计每位同学选修的课程数量题目大概意思是输入若干行每行是“课程名 学生姓名”要求统计每位学生选修了多少门课最后按学生姓名字典序输出。这道题考的就是基础字典统计但要注意键是学生名值是课程数量同一门课同一个学生可能出现多次需要去重。我当时的AC代码是这样写的import sys d {} for line in sys.stdin: line line.strip() if not line: continue course, name line.split() # 利用集合去重保证同一课程同一学生只计一次 if name not in d: d[name] set() d[name].add(course) for name in sorted(d): print(name, len(d[name]))为什么要用集合set()而不是列表因为一门课可能重复出现在输入里对同一个学生来说重复的课程不应该重复计数。用set天然去重最后直接len()就是课程数量省去了手动判断重复的逻辑。这里的坑在于有些同学会想用d[name] d.get(name, 0) 1这样如果同一行重复出现就会多计数。评测数据里偏偏就会给你这种重复行专门卡不带去重的解法。所以这个案例的核心不是统计数量而是“去重后再统计”。另外注意读入方式。在PTA上输入的行数不确定用sys.stdin按行循环是最稳定的写法。如果你用input()配一个固定的循环次数很容易因为输入行数多于预期而处理不完整或者空行导致split()出问题。3.2 案例二词频统计并输出出现次数最多的前N个这道题是典型的字典统计加排序。题目给一段英文文章要求统计单词出现次数输出频率最高的几个词同频率时按字典序升序。这道题难倒不少人的是中文题目经常说“按出现次数从高到低、字典序从小到大”这种双条件排序。我的AC代码简化后是这样import sys text sys.stdin.read().split() cnt {} for word in text: cnt[word] cnt.get(word, 0) 1 # 先按次数降序再按单词字典序升序 items sorted(cnt.items(), keylambda x: (-x[1], x[0])) top_n int(input()) # 通常是前N个 for word, num in items[:top_n]: print(f{word}:{num})这里的核心是排序键(-x[1], x[0])。第一个元素写成负数就能让次数降序排列第二个元素是单词本身字典序升序。很多人会写keylambda x: (x[1], x[0])结果发现频率是升序与要求相反。所以这个-x[1]其实是个小技巧sorted默认升序你不想reverseTrue影响字典序部分的话就可以用负数来伪装降序。另一个容易出错的地方是分割单词。split()默认按任意空白字符分割可以处理空格、换行、Tab这在绝大多数英文文章题里都够用。但如果有标点符号粘连比如hello,就需要使用正则或者清洗标点的步骤。PTA的这类题目通常出题时会避开这样的复杂情况因为它主要考字典排序而不是字符串清洗。3.3 案例三求两个集合的交集、并集与对称差集合运算题在第六章是送分题但如果你不会用运算符就会写出冗长的循环。PTA常见题目是输入两个集合的元素输出交集、并集、补集或对称差。Python集合运算符可以直接搞定a set(map(int, input().split())) b set(map(int, input().split())) inter a b # 交集 union a | b # 并集 sym_diff a ^ b # 对称差a中有或b中有但不同时有输出时注意题目要求通常要排序输出而且元素之间没有多余的空格或按指定格式。比如“输出集合元素个数以及排序后的元素列表”。我的做法是print(len(inter)) print( .join(map(str, sorted(inter))))这里的坑有两个第一sorted(inter)是升序列表如果你直接遍历集合输出的顺序完全随机评测机一比较字符串就报WA答案错误第二map(str, ...)是把每个数字转成字符串因为join只能拼接字符串。很多同学忘了转字符串直接 .join(sorted(inter))然后程序报TypeError。这种错误属于格式坑在PTA上特别常见。4. 我踩过的坑和排查技巧实录前面讲的是通解下面这部分是我真在金六章做题时摔过的跟头。很多问题不看评测反馈根本想不到我把它们整理成话放在这里能帮大家少走不少弯路。4.1 遍历字典时不要直接改字典我最早写第六章一道题时需要在统计完成后删除某些不符合条件的键我直接写for k in d: if d[k] 2: del d[k]结果一提交报RuntimeError: dictionary changed size during iteration。这个错误翻译过来就是“遍历字典的过程中字典的长度变了”。Python不允许你在迭代它的时候增删键值对因为这会破坏内部结构。如果你真要去除部分元素正确做法是先收集要删除的键循环结束后再统一删除to_remove [k for k in d if d[k] 2] for k in to_remove: del d[k]或者直接用字典推导式生成新字典d {k: v for k, v in d.items() if v 2}这个坑在PTA上不会立刻暴露因为你可能数据小的时候侥幸没事但只要有一次遍历中恰好删了键程序就直接崩。记住了迭代时不要改容器想过滤就构建新字典。4.2 集合与字典的键要求“可哈希”第六章函数题里偶尔会要求把列表作为字典的键。很多同学下意识写成d[list1] 1结果报TypeError: unhashable type: list。因为列表是可变类型不能作为键。这时候要么把列表转换成元组要么把列表转成字符串。遇到这种题我一般会把列表转元组因为元组不可变、可哈希。例如key tuple(sorted(lst)) d[key] d.get(key, 0) 1如果你用str(lst)也能当键但这样会丢失结构信息后续恢复比较麻烦。优先用元组。另外集合本身也是不可哈希的不能作为字典键但frozenset可以。PTA的题目里很少需要用到frozenset知道这一点就够了。4.3 输入读取选择直接决定是否超时第六章有些题目数据量很大比如上万行输入。如果你在循环里用for _ in range(n): line input()其实也能过因为input()本身并不算慢。最致命的写法是在循环里不停拼接字符串result for x in data: result str(x) print(result)这在一万次循环里没问题但如果是十万、百万级的数据字符串是不可变对象每次都会生成一个新的字符串把旧字符串整体拷贝一遍。复杂度瞬间变成O(n^2)测试点一大就直接TLE。更好的方式是用列表收集再统一joinparts [] for x in data: parts.append(str(x)) print( .join(parts)) .join(parts)是线性复杂度即使数据量大也扛得住。这个习惯如果从第六章养成到后面写复杂题目时收益很大。4.4 输出格式里最容易被忽略的空格、换行与冒号PTA的判断题和程序填空题暂且不提选择题和编程题里输出格式错一点就是白给。第六章经典的格式坑包括要求“每个数之间用一个空格隔开行末不能有多余空格”。很多人的代码是循环里print(each, end )这样最后一个元素后面也有空格评测机会对比输出字符串所以直接WA。正确做法是用join拼接或者用索引判断。要求输出“键: 值”冒号后面有空格。有人写成f{k}:{v}差一个空格就是格式错误。每次都读清楚题目再动手不要自己脑补。要求“每个输出占一行”但有的人用end 把所有结果挤在一行。输出换行是最容易检查的提交前先在本地把所有样例跑一遍肉眼核对一次即可。我后来把这种格式问题总结成一个自查动作写完代码先不急着提交手动构造简单输入跑一下然后对比题目给的样例输出逐字符看一遍。这个动作浪费不了30秒但能避免一半以上的非算法错误。5. 从AC到会写的进阶建议第六章的题不算难但刷完不等于你会写这类代码。我从自己带人刷题的经验来看不少同学AC了一堆题但过两天拿到新题还是不会做。问题出在只背了答案没有形成自己的解题流程。5.1 拿到题目先画出数据关系我现在每做一道题第一件事不是写代码而是在草稿纸上画出输入数据的格式和期望输出的格式然后用一条线把从输入到输出的映射关系标出来。比如统计题就是从一条记录中取出键、取出值更新字典去重题就是从原始序列中筛掉seen里的元素。这个映射图一旦画出来代码就是把图中的每一步翻译成Python。这个方法听起来简单但特别管用。因为第六章的题大多数据结构单一最难的不是语法而是理不清该用什么容器、键是什么、值是什么。画完图你一眼就能看到“这里需要一个集合来去重”“那里需要一个字典来计数”。5.2 用本地造数据代替瞎猜如果某道题提交后WA了第一反应不要是重新读题而是先本地验证。我会自己构造一组包含重复值、边界值、空值的小数据一步步跑代码看哪里和预期不符。比如英文词频题我会造一句话里出现大小写不同、重复单词、标点符号附近的情况然后看代码输出是否符合题意。本地验证的好处是你能逐步打印中间值。养成随手画调试输出的习惯在循环里插入一句print(d)看统计过程是否正确。这比你瞪着眼睛看代码逻辑更有效果。5.3 给自己留一个常用API自查表第六章你只需要掌握十几个函数和操作真的不多。我不建议背所有函数但建议你归纳一份自己的小表单比如字典get、items、keys、values、setdefault、update集合add、remove、discard、union、intersection、difference列表转元组tuple(list)字典推导式{k: v for ...}集合推导式{x for x in ...}按值排序sorted(d.items(), keylambda x: x[1])这个表格不一定全面但它覆盖了PTA第六章至少70%的题目所需的操作。做题卡住的时候先查表比上网翻文档更快。最后分享一个我个人的小习惯每次AC一道题以后我会把代码里最关键的那一行单独抄进一个笔记文件里旁边写上这一行解决了什么问题。这样积累一段时间以后你会发现你已经形成了一种“看到题目就能反射出数据结构”的能力。刷题的意义不是AC本身而是让你以后写任何脚本、处理任何数据时下意识选择最合适的容器和操作。把第六章吃透后面再遇到文件读写、函数式编程和面向对象的内容你会轻松很多。