news 2026/8/29 6:04:45

从牛客一模看集合编程题:递归建模、BFS判重与哈希优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从牛客一模看集合编程题:递归建模、BFS判重与哈希优化

这套2019牛客一模的编程题,我印象一直挺深。不是因为题目有多难,而是它把"集合"这个最基础的数据结构从头到尾考了一遍:有递归定义的集合,有藏在数学背景里的自然数集合,还有需要用哈希集合优化暴力的题目。当时在牛客上做完这一套,最大的感受是——真正拉开差距的不是会不会某个经典算法,而是能不能在看懂题目之后,迅速把问题翻译成"集合操作"的思路。

这篇文章不是官方题解,是我自己当年参加一模的复盘。我会把题目形态、解题思路、代码细节和踩过的坑整理出来,尤其会围绕"集合"这条主线展开。如果你正在准备春招秋招,或者想找一套有代表性的编程题检验自己的水平,这套题值得认真做一遍。看完你至少能明白:在线笔试里的集合题,考点到底藏在哪。

1. 试卷整体印象:四道题的难度阶梯和我的时间分配

1.1 题型分布与难度阶梯

2019年牛客一模的编程题,整体排布是比较典型的上阶梯结构。印象里四道题,大体可以分成三个档次。

题号题目形态核心考点难度感知
第一题签到题,字符串或基础数组操作输入输出、简单模拟热身级别
第二题数学背景的构造题数论推导、等差数列公式中等偏易
第三题集合定义类题目递归展开、集合判重、BFS中等偏难
第四题数据结构和算法的综合应用图论或动态规划结合集合去重压轴

这个排布非常典型:前面的题保底分,后面的题拉开区分度。不要小看第一题,在线笔试里"签到题"翻车的人每次都有,大部分原因是读题不仔细,把多组输入当成了单组输入。

1.2 我的做题顺序:先吃软柿子,再啃硬骨头

开考之后我先把四道题都扫了一遍,这种做法救了我。第二题和高斯求和有关,一看就是公式题;第三题是递归定义的集合,核心在于建模;第四题明显需要更多的思考时间。我当时定的策略是:第一题十分钟内解决,第二题二十分钟解决,第三题主攻,第四题如果有剩余时间再碰。

整套卷子做完复盘,我发现一个规律:前三题几乎都围绕"集合"这个概念展开。这就引出了一个很重要的问题——为什么一模的编程题集体指向集合?

2. 为什么一模把"集合"当成核心考点

2.1 集合在笔试题里的三种出场方式

刷题多了你会发现,集合在编程题里一般有三种出场方式。

第一种是显式考集合。题目会直接定义一个集合,让你判断某个元素是否属于它、求集合大小、求交集并集。这种题表面上在考数学定义,实际上考的是怎么把定义翻译成代码。

第二种是工具型考集合。题目本身和集合无关,但解法中必须用集合做去重或标记。比如图论里的visited数组、动态规划里的状态判重,本质都是集合思想。

第三种是语言层面考集合。这种更多出现在面试题里,比如Java面试喜欢问HashSet和TreeSet的区别、底层数据结构,C#面试会问怎么创建一个集合并保证不重复。虽然笔试不一定直接考语言API,但你在写代码时选择的集合类型会直接影响复杂度和正确性。

一模这套题,恰好把三种出场方式都覆盖了。所以做完之后你会感觉"集合"这个词无处不在,这不是巧合,是出题人刻意设计的。

2.2 "集合"考的不是API,是建模能力

很多同学看到集合题,第一反应是"我会用set,我会用HashSet,题目稳了"。但这套题告诉你:会用API远远不够。

比如"以a为基的集合Ba"那道题,题目给的是一组递归规则,你看着像数学题,但本质上要你做的,是把一句"若x属于集合,则f(x)也属于集合"翻译成一个可执行的状态扩展过程。这里思路如果没转过来,就会卡在"怎么用集合表示无限元素"这个点上。

再比如高斯自然数集合那道题,看起来是求和,实际上要你意识到"连续正整数集合"的数学结构,再用等差数列公式去优化。这里考察的是从具体例子中抽象出数学模型的能力,这和纯粹的API调用完全是两码事。

所以我把这套题的价值总结成一句话:它逼你把集合从"数据结构"升级成"思维方式"。这才是笔试真正想挑出来的能力。

2.3 在线笔试环境对集合题特别友好

还有一个容易被忽略的因素:在线笔试的判题系统对集合题很友好。因为集合操作的结果是确定的,不涉及浮点数精度、不涉及随机化、不容易产生歧义,非常适合机器判题。

这也解释了一个现象:模考卷子里集合题多,不是偶然,而是出题人为了保证题目质量、控制判题难度,会优先选择这类结果明确的题。

3. Ba集合:递归定义下的集合到底怎么建模

3.1 题目还原:你看到的定义和实际要算的东西

这道题我当时拿到手,题目大概长这样:

对于以a为基的集合Ba,定义如下: (1)a属于Ba; (2)若x属于Ba,则 x+a 和 x*a 也属于Ba; (3)Ba是满足上述条件的最小集合。 给定 a 和 n,判断 n 是否属于Ba;若属于,输出它是由多少步生成的。

第一眼看上去,集合元素是无限多的:a, 2a, a², 3a, a³……越往后膨胀得越厉害。如果没转换思路,很容易陷入"把所有元素都生成出来"的误区。

其实题目真正要你做的,是沿着规则进行状态扩展。这本质上是图的遍历:每个集合元素是一个节点,规则(1)是起点,规则(2)是边的定义,"判断n是否属于Ba"就是在问:从a出发,经过若干次"加a"或"乘a"操作,能不能走到n。

3.2 BFS展开加set判重:最稳的解法

建模思路定了之后,解法就很直接了:从a出发做BFS,每一步生成两个新状态 x+a 和 x*a,用set记录已经访问过的元素,防止重复扩展,扩展过程中如果遇到n就说明n属于集合。如果元素值超过n,就直接剪枝——因为加法和乘法都是递增操作,继续扩展只会更大,不可能回到n。

from collections import deque def can_reach(a, n): if n < a: return False if n == a: return True q = deque([a]) visited = {a} while q: x = q.popleft() for nxt in (x + a, x * a): if nxt == n: return True if nxt < n and nxt not in visited: visited.add(nxt) q.append(nxt) return False

这个代码里set承担了两个职责:一是判重,防止同一条路径反复扩展;二是标记,保证算法的复杂度是O(n)级别的,而不是指数级的。

当时我写完之后自己测了几组数据,发现一个关键点:只要n能表示成 a^k 或 某个a的倍数组合的形式,算法就能找到。这让我意识到,集合定义题表面上考规则,实际考的是你有没有想到用BFS去"生成"集合。

3.3 考场上容易翻车的三个细节

第一,乘法的爆炸速度。如果n是10^9级别,x*a可能在一次扩展后就远远超过n。所以扩展时一定要先判断 nxt < n 再入队,否则队列会越积越大,直到内存撑爆。

第二,重复扩展的问题。如果不做visited标记,同一个元素会被多条路径访问到。比如 a=2 时,2+2 和 2×2 都等于4,两条路径都会生成4,第二次生成时如果没有set判重,就会重复入队,指数级膨胀。

第三,递归和BFS的选择。有些同学习惯写DFS递归,但这道题的状态空间可能是环形的——x+a 和 x*a 的结果之间可能互相到达,递归深度不可控,容易栈溢出。BFS配合显示队列更稳。

4. 高斯自然数集合:把求和公式变成解题工具

4.1 题目背景与高斯求和的迁移

看到"高斯"两个字,第一反应应该是1加到100等于5050的故事。这道题也确实用到了等差数列求和公式。

题目大概是说:高斯发现,任意一个正整数都可以拆成若干个连续正整数的和。比如 9 = 2+3+4 = 4+5,而 8 就没有任何连续正整数拆分。给定 n,求有多少种不同的拆分方式。

这里"连续正整数集合"是解题的关键。连续正整数的和从首项a开始、长度为len时,和可以写成:

sum = len × a + len × (len - 1) / 2

这个公式就是高斯求和公式的变形。推导过程很简单:len个连续整数的和是 len × a + (0 + 1 + ... + len-1),后面的括号里正好是 len×(len-1)/2。

4.2 从暴力到数学优化:枚举长度而不是枚举起点

第一直觉是枚举起点a,然后往里加数,判断和是否等于n。但这么做复杂度是O(n²),n一大就废了。

换个思路:枚举长度len,通过公式反推起点a是否存在

根据上面的公式,给定len之后:

a = (n - len × (len - 1) / 2) / len

要让a是正整数,需要满足两个条件:

  1. n - len×(len-1)/2 必须大于0;
  2. 这个差值必须能被len整除。

于是我们可以写出完整的代码:

def count_ways(n): ans = 0 len_ = 2 while len_ * (len_ - 1) // 2 < n: remain = n - len_ * (len_ - 1) // 2 if remain > 0 and remain % len_ == 0: ans += 1 len_ += 1 return ans

len的最大值范围也很好估算:因为a最小是1,所以 n ≥ len×(len-1)/2,也就是 len 大约在 sqrt(2n) 量级。对于10^9的n,只需要枚举到大约45000个长度,复杂度可以接受。

4.3 边界情况和输出格式的坑

这道题代码写对不难,但边界情况特别容易出错。

当 n=1 时,没有任何长度满足条件,答案是0。当 n=2 时,同样没有满足条件的长度。这个当时很多同学没注意。

还有一点,题目如果要求输出具体的拆分方案,那就不能只计数了。需要在把可行的len筛出来之后,再计算对应的首项a,然后循环输出a到a+len-1的区间。这里要注意输出格式,数字之间用空格分隔、末尾是否允许有多余空格,在线判题对空格非常敏感。

我当年在这题上犯过一个低级错误:输出答案是"0"而不是换行,导致格式错误。所以大家做题时一定要确认,输出的每个数字后面跟的是空格还是换行,最后一组数据后面要不要加换行。

5. 编程语言的集合实现细节:Python/Java/C++横向对比

5.1 Python的set:笔试中最省心的集合

用Python写集合题,体验是最好的。set自带去重、交集、并集、差集操作,写起来几乎就是数学语言,基本不用关心底层实现。

s = {1, 2, 3} t = {3, 4, 5} print(s & t) # 交集 {3} print(s | t) # 并集 {1, 2, 3, 4, 5} print(s - t) # 差集 {1, 2}

需要注意的一点是frozenset。Python的set是可变的,不能作为另一个set的元素。如果你想做一个"集合的集合",必须把内部集合转成frozenset。这个问题在做某些题目时会出现,我当时第一次碰到还愣了一会儿。

元素必须是可哈希的,这意味着list不能放进set。如果需要用list做元素,先转成tuple。这些细节平时写脚本无所谓,笔试的时候一旦踩到就是运行时错误。

5.2 Java和C++:有序集合和无序集合的选择

Java里用HashSet还是TreeSet,C++里用set还是unordered_set,底层机制完全不同。

HashSet和unordered_set底层是哈希表,查找、插入、删除平均O(1),但元素没有顺序。TreeSet和set底层是红黑树,操作是O(log n),但元素保持有序。

笔试中如果题目只要求判重,优先用HashSet/unordered_set,因为常数更小。但如果题目要求输出有序的集合结果,比如从小到大输出所有不重复元素,用TreeSet/set会省很多事,省去手动排序。

Java的HashSet里面存放自定义对象时,需要重写hashCode和equals方法,否则集合判重会按对象地址判断,导致两个内容相同的对象都被放进去。笔试里经常有人在这上面翻车,尤其是定义了一个坐标类Point然后想对它去重时。

5.3 "集合输出乱码"这个坑是怎么踩出来的

热词列表里有一条"集合输出是乱码",我一看就想起来自己确实踩过这个坑,而且不止一次。

第一次是用C++写set的时候,set里存的是char*。这个坑非常隐蔽:C++的set在比较两个char*时比较的是指针地址而不是字符串内容,所以两个内容相同的字符串会被当成不同元素。更离谱的是,如果用printf直接输出set里的char*,遇到中文字符串在某些终端下就会显示乱码。

第二次是Java的System.out.println直接打印HashSet,集合里的中文对象如果没有正确设置编码,Windows控制台上就可能显示为乱码。解决方法是启动参数加-Dfile.encoding=UTF-8,或者干脆用循环逐个输出元素,不要直接println整个集合。

写到这里我想多说一句:笔试环境一般不会让你调试太久,遇到这种"看起来和算法无关"的玄学问题,最稳妥的做法是确保集合的泛型类型是字符串对象而不是字符数组,同时在本地就配置好UTF-8编码。这种细节看着小,但真的能卡掉一大半人。

6. 从模考到面试:集合考点还能怎么延伸

6.1 集合的底层实现和面试追问

一模考完之后,如果只是把题改完就扔一边,那这套题的价值就只发挥了一半。更好的做法是把"集合"这条线延伸成一套完整的面试知识点。

Java面试里关于集合的经典追问包括:HashSet的底层是怎么实现的?为什么用HashMap就能实现HashSet?TreeSet的排序和比较器是怎么回事?ConcurrentHashMap为什么不能存null键值?这些问题看起来是语言问题,其实核心还是"集合如何保证不重复、如何保证有序"这两个基础点。

我后来在准备面试时,把一模的集合题整理成一页纸的笔记:笔试里的set用来判重,面试里的Set用来考哈希原理。两者殊途同归。

6.2 多重集合排列的计数模板

热词里有一条"多重集合排列",这个知识点也值得一提。

有时候题目会给一个包含重复元素的集合,问这些元素能构成多少种不同的排列。公式是:

n! / (cnt1! × cnt2! × ... × cntk!)

其中cnt1到cntk是每种重复元素的个数。这个公式看起来简单,但配合大数取模时,需要预处理阶乘和逆元。笔试中这种题出现的频率不低,而且经常藏在"字符串重排""字母统计"之类的外壳下面。

更好的消息是,Python的math.comb可以直接做组合数计算,配合循环就能实现多重排列计数,不需要自己写逆元。刷题时合理利用语言特性,能省下不少时间。

6.3 刷完这套题,我建议你顺手做的两件事

第一件事,把每道题的输入输出部分单独提取出来,改造成可以复用的一套模板。比如统一的读取整数、读取一行字符串、输出用空格分隔的数组等函数。在线笔试时间紧张,能少敲几行代码都是好的。

第二件事,把BFS配合set判重的代码背下来。这套模板在集合定义题、状态搜索题、迷宫题、字符串变换题里通用性极强。你只需要做的,是定义清楚状态是什么、规则是什么、terminate条件是什么,剩下的就是套模板。

聊到这儿,"集合"这条主线差不多就串完了。有些人刷题追求数量,一套一套往下刷,但迟迟不见长进。问题往往出在人没有停下来做归纳:同样是集合,递归定义的集合考的是建模,高斯自然数集合考的是数学公式和枚举优化,语言层面的集合考的是底层原理。归纳完之后你会发现,你刷的不是一道一道的题,而是一类一类的解法。

这套2019一模的题目,技巧不算高深,胜在典型。我建议你找一个完整的时间段,按笔试的标准时间把四道题过一遍,然后对照我今天写的思路重新整理自己的解法。尤其是Ba集合那题,真正能把BFS加set玩顺了,再遇到任何"按规则生成集合"的题目,你都会觉得不过如此。

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

Git分支按提交时间排序:从命令到分支治理的完整指南

接手一个有年份的仓库时&#xff0c;真正让人头疼的往往不是代码写得烂&#xff0c;而是分支多到你不知道该看哪条。我在工作中见过不止一次这样的场面&#xff1a;一个项目的本地分支二十多个&#xff0c;名字里有 feature、fix、test、release、backup、old、final&#xff0…

作者头像 李华
网站建设 2026/8/29 6:03:03

基于PyBullet与Stable-Baselines3的机械臂抓取强化学习实战

简介&#xff1a;强化学习在机器人控制领域的应用日益广泛&#xff0c;但直接在真实机械臂上训练成本高、风险大&#xff0c;仿真训练成为降低试错成本的关键手段。PyBullet作为轻量级物理仿真引擎&#xff0c;凭借其Python友好接口和与Gym环境的无缝集成&#xff0c;成为快速搭…

作者头像 李华
网站建设 2026/8/29 6:00:28

B样条轨迹生成:面向无人机飞行走廊的Jerk连续平滑规划

简介&#xff1a;本资源是一套面向无人机路径规划研究者与ROS开发者实现飞行走廊轨迹优化的完整工程代码&#xff0c;聚焦B样条曲线建模与动力学约束下的平滑轨迹生成&#xff0c;适用于物流配送、巡检避障、空域协同等高实时性场景。压缩包共160个文件&#xff0c;含24个核心C…

作者头像 李华
网站建设 2026/8/29 5:57:07

谷歌微软阿里美团实习面试全复盘:流程差异与备战攻略

四家公司的实习生面试轮下来&#xff0c;我最大的感受是&#xff1a;谷歌、微软、阿里、美团虽然都在招“实习生”&#xff0c;但考察重心和面试节奏差异非常大。谷歌和微软更看重算法底子和沟通推导过程&#xff0c;阿里会追着项目细节一层层往下挖&#xff0c;美团则典型地考…

作者头像 李华
网站建设 2026/8/29 5:55:56

财政科学管理:存量重组与效率红利深层逻辑

《财政积极的秘密&#xff0c;藏在账房里》 ——加码是态度&#xff0c;算账是胜负&#xff1b;这一轮逆周期&#xff0c;比的是谁把钱管得科学先猜一道题&#xff1a;下半年财政要“更加积极”&#xff0c;会议纪要里出现最多的字&#xff0c;是“花”&#xff0c;还是“管”&…

作者头像 李华
网站建设 2026/8/29 5:55:53

RK3588架构 边缘AI视觉04-零拷贝跨进程通信

RK3588架构 零拷贝跨进程通信&#xff1a;边缘服务和算法服务之间如何做到近零 CPU 负载的图像传输上一篇我们分享了同源多任务调度&#xff0c;让一路视频流同时支撑多个算法任务。这一篇我们深入到进程间通信——三进程架构下&#xff0c;边缘核心服务和推理引擎是两个独立进…

作者头像 李华