最近整理旧硬盘里的刷题记录,翻到牛客网 Nowcoder Girl 2017 的题目集合,突然有点感慨。那会儿这套题还是专为女生编程比赛设计的题目,题目本身并不算难,但胜在覆盖面广、出题风格干净,很适合作为算法入门和面试前热身的题单。现在网上还能源源不断搜到这套题的题解,但大多是零散的答案罗列,很少有人把整套题的考点结构、解题思路和踩坑点串起来讲。这篇就当是我的个人复盘,给准备刷题的同学一个参考。
先把结论放在前面:如果你正在准备春招秋招,或者刚开始学算法但不知道从哪里下手,这套题目集合是个不错的起点。它不需要你有竞赛背景,也不需要掌握特别冷门的数据结构,只要把数组、字符串、模拟、排序、贪心、简单动态规划这些基础吃透,就能拿下大部分题目。而且因为当年比赛是面向女生的,题目设计上少了很多“劝退”式难题,整体多了一份友好,对新手特别合适。
提示:我下面讲到的题目类型和解法,是基于这套题集的整体风格和常见出题方向做的复盘,不是逐题复述,具体题目细节建议以你下载到的题面为准。
1. 这套题目集合到底考什么:考点分布与难度曲线
1.1 题目规模与整体风格
Nowcoder Girl 2017 的题目集合,整体规模不算大,大概是五六道编程题的量级,比赛时长通常控制在两到三小时。这个体量对新手来说非常友好,不会像 ACM 区域赛那样动辄五小时、十道题起步,刷起来心理压力小很多。但你千万别因为题量小就轻视它——题目数量少,意味着每一道题的分量都更重,考的知识点更集中,没有太多“水题”给你凑数。
从整体风格来看,这套题的出题人明显是想考察“基础算法能力 + 工程实现细节”,而不是比拼谁会的偏门数据结构多。题目描述通常比较直白,没有一大堆故事背景包装,输入输出格式也规范,不会故意在格式上挖坑。这一点对初学者特别重要,因为刷题最怕的不是不会做,而是被题目描述绕晕,连题意都读不懂。
我当年刷这套题的感受是:它把“算法竞赛”和“面试算法题”之间的尺度把握得比较好。它不会像 LeetCode 那样每道题都偏重某一个固定套路,也不会像 ACM 那样需要很强的数学功底和代码熟练度。它就是老老实实地考察你能不能把一道题拆解清楚,用合适的算法解决,然后把代码写对。
1.2 难度曲线是怎么设计的
这可能是 Nowcoder Girl 2017 题目集合最值得称道的一点:难度曲线设计得很平滑。前面一两道题基本是送分题,你只要会读入输出、会写简单循环、理解基本的数组操作就能拿到分。中间的题开始有些思考量,需要你分析问题的性质,选择合适的数据结构或算法策略。最后面的题则有一定的区分度,通常涉及动态规划或更复杂的贪心证明,用来筛选真正有算法思维的人。
这种难度曲线对刷题者的指导意义是什么?我觉得是:你可以用它来给自己的水平做一次“定位检测”。如果你能在前几道题上不卡壳、一遍过,说明你的基础语法和简单逻辑没问题;如果你能顺利搞定中间难度的题,说明你已经具备基本的算法思维;如果你能独立做出最难的题,那恭喜你,你的水平已经超过大部分应届求职者了。
我当时给自己的定位就是“能做对中间难度的题,难题需要看题解”,于是针对性地补了动态规划和贪心的短板,这个定位方法后来也沿用到了其他刷题平台上。
1.3 考点覆盖情况一览
我复盘了这套题涉及的考点,大概可以分成下面这几类,我用一个表格列出来,方便你对照自己的薄弱点。
| 考点类别 | 常见出题形式 | 需要掌握的基础 |
|---|---|---|
| 字符串处理 | 字符串匹配、子串计数、格式转换 | 字符串基本操作、字符数组、大小写转换 |
| 模拟 | 按规则一步步执行流程 | 循环、条件分支、状态标记 |
| 排序与查找 | 结构体排序、二分查找 | 排序算法、比较函数、二分边界 |
| 贪心 | 区间问题、排序后取最优 | 贪心策略证明、排序配合 |
| 简单动态规划 | 背包、路径计数、经典线性模型 | 状态定义、转移方程、空间优化 |
| 数学与枚举 | 素数判断、组合计数、暴力枚举 | 数论基础、循环嵌套、剪枝 |
你会发现,这些考点没有一个是超纲的,全部是大学计算机专业低年级课程或者自学教程里会出现的内容。所以我一直认为,这套题目集合更适合作为“入门后的第一套完整题单”,而不是零基础的第一课。如果你连循环和数组都还没写利索,建议先去学完基础语法再来刷,这样它的价值才能最大化。
2. 刷题之前的准备:语言选择、环境搭建与答题策略
2.1 选哪门语言上考场
我见过很多人在刷题前纠结语言选择,实际上这个问题的答案挺简单的:选你最有把握的那门。Nowcoder Girl 2017 这套题并不限制语言,C++、Java、Python 都能用,Online Judge 系统的判题核心是看你的输出对不对,而不是看你会哪门语言。
不过从实战效率角度来看,我个人的建议是:初学者优先考虑 C++ 或 Java,已经刷过一定量题目的人可以用 Python。为什么这么说?因为这套题里的动态规划和复杂模拟,Python 写起来确实更简洁,调试成本低;但 C++ 在输入输出和运行速度上有天然优势,不容易因为常数问题超时。而 Java 则介于两者之间,代码量比 C++ 多一点,但容错率高,不容易踩指针和内存的坑。
如果你是女生,看到这个比赛名称心里可能会有点“是不是男生不适合做这套题”的疑问。我明确告诉你,完全没有这回事。比赛标题只是一种主题命名,题目的算法内容和性别没有任何关系,男生女生都可以拿来刷。它就是一个普通的算法题单,千万别被名字劝退。
2.2 本地环境怎么搭,才能减少提交失败
很多初学者有一个坏习惯:直接在 Online Judge 的网页文本框里写代码,写完就提交,报错了再回来看。这个流程在题目简单时还行,一旦遇到稍微复杂的逻辑,效率会低得让人抓狂。我刷这套题时的习惯是:本地先搭一个干净的开发环境,把所有题目当作一个小型工程来做。
推荐的最小环境配置是:电脑上装好你选择语言的编译器/解释器,再加一个支持语法高亮的编辑器。如果你用的是 C++,推荐装 Code::Blocks 或者 VS Code + MinGW;Java 推荐 IntelliJ IDEA Community 或者 Eclipse;Python 则只需要装好解释器,用 VS Code 或者直接 IDLE 都可以。你不一定需要特别复杂的 IDE,但一定要能方便地写代码、跑测试、看报错信息。
另外一个非常实用的习惯是:每道题新建一个独立的文件夹或者文件,把题目描述复制粘贴到注释里,再把样例输入输出保存为单独的文本文件。这样做的好处是,当你调试时可以直接用< input.txt重定向输入,不需要每次手动敲样例数据。尤其是这套题里有几道输入格式比较复杂,手动输入很容易漏掉空白行或多余空格,用文件重定向能彻底避免这个问题。
注意:本地跑通样例不等于代码一定正确。Online Judge 的测试数据通常比样例更多、更刁钻,所以本地验证只是第一步,后面还要自己构造边界情况。
2.3 一场比赛的时间分配策略
如果你想把这套题集合当作模拟考试来做,我建议你给自己设定两小时的时间上限,然后按下面的策略分配:
前 20 分钟,快速浏览所有题目,把每道题的数据范围、输入输出格式、大致难度标出来。不要在第一道题上死磕太久,如果连续 15 分钟没有思路,果断跳到下一题。中间 60 到 80 分钟,按“先易后难”的顺序做题,先保证把所有有把握的题都写出来拿到分,再回头啃难题。最后剩下 20 到 30 分钟,专门用来检查边界条件和数据范围,宁可少做一道题,也不能因为没考虑边界而丢掉已经写出来的题的分数。
我在模拟这套题的时候就吃过亏,一道字符串题因为没考虑到字符串长度为 1 的特殊情况,导致边界测试点全挂,白白丢了 20 分。从那以后,我再也不敢把“时间全部压在难题上”作为策略了。
3. 题目类型拆解:五类高频考点的解题思路
3.1 字符串处理:边界与格式的坑
字符串处理是 Nowcoder Girl 2017 题目集合里出现频率很高的考点。这类题本身算法难度不高,但特别考验细节,尤其是边界和格式。常见的形式是:给你一个字符串或一组字符串,要求做某种统计或转换,比如统计字符出现次数、判断子串、按规则做替换等。
做字符串题,我总结了三句话:先看输入长度,再看字符集,最后想清楚空字符串怎么处理。输入长度决定你能不能暴力,比如长度在 1000 以内,O(n^2) 的双重循环完全没问题;如果长度到了 10^5,你就要考虑 O(n) 或者 O(n log n) 的做法。字符集决定你开数组的大小,是只用处理英文字母,还是要处理数字和标点。空字符串的情况容易被人忽略,但出题人最喜欢在这里埋坑。
举个例子,一道常见的题目形式是“将字符串中的连续重复字符压缩为字符加数字”,比如aaabbc -> a3b2c1。这题的核心思路是维护当前字符的计数,遍历到不同字符时,把前一个字符和计数拼接到结果里。看起来简单,但真正写的时候,很多人会在遍历结束后忘记处理最后一组字符,导致输出少了最后一段。这类边界问题是字符串题丢分的重灾区,写代码之前先在草稿纸上走一遍“只有一个字符”和“没有任何字符”两个极端样例,基本就能避开一半的坑。
string compressString(string s) { if (s.empty()) return ""; string res = ""; int cnt = 1; for (int i = 1; i <= s.size(); i++) { if (i < s.size() && s[i] == s[i - 1]) { cnt++; } else { res += s[i - 1]; res += to_string(cnt); cnt = 1; } } return res; }这段代码的关键在于i <= s.size()这个循环条件,它保证了遍历结束后,最后一组字符一定会被处理到。这种“多走一步”处理尾部的方式,是字符串题里非常常用的技巧,你在做其他题目时也可以借鉴。
3.2 模拟题:先画流程图再写代码
模拟题在 Nowcoder Girl 2017 里占了不小的比重。这类题的特点是没有太高深的算法,核心就是把题目描述的过程一步步用代码实现出来。听起来简单,实际上做起来特别容易乱,因为题目里往往包含多个状态、多个分支、多个角色的交互。
我的建议是:遇到模拟题,第一件事不是打开编译器写代码,而是先拿纸笔把流程图画出来。比如题目要你模拟一个排队系统,那你至少要想清楚:队伍怎么存?是先进先出还是优先队列?什么事件会导致队伍状态变化?状态变化之后要输出什么?把这些都画清楚,再开始写代码,效率会高出一大截。
其实模拟题最考验的不是“写代码”的能力,而是“拆解规则”的能力。题目描述通常是一大段文字,你要从中提取出关键的规则点,把它们转换成代码里的变量和流程。我见过不少同学,代码写得挺熟练,但遇到模拟题就大脑空白,原因不是不会写,而是没有养成“提取规则 → 设计状态 → 实现流程”的思维路径。
打个比方:模拟题就像你照着菜谱做菜,菜谱里写着“小火煮至浓稠”,你要自己判断什么是小火、什么是浓稠;如果菜谱里写着“加入少许盐”,你要自己决定放多少。代码里没有“少许”这种模糊概念,你必须把每一步都量化成具体的条件。
3.3 贪心:证明比代码重要
贪心算法是 Nowcoder Girl 2017 题目集合里区分度比较高的一块。贪心的代码往往很短,可能十几行就能写完,难的是“为什么这样贪是对的”。比如经典的区间调度问题,按结束时间排序然后依次选择最早结束的区间,这个策略背下来容易,但为什么不能按开始时间排序?为什么不能按区间长度排序?想清楚这些,你才算真正掌握了贪心。
我看题解时发现一个有意思的现象:很多人看贪心题的题解,只看“怎么做”,不看“为什么”。于是下一次遇到类似的题,换了个场景就不知道能不能用同样策略了。这颗种子在刷题阶段埋下,笔试面试时就会结出苦果,面试官最爱追问的就是“你凭什么叫这个贪心策略是最优的”。
学习贪心,我建议你至少在草稿纸上做三件事:第一,明确你每一步贪心选择的依据是什么;第二,构造一个反例,证明“如果我不这么贪,结果会怎样”;第三,试着用简单的数学归纳法或交换论证去说明这个策略不会比最优解差。你不需要写出严格的数学证明,只要能说服自己“这个策略在逻辑上站得住脚”,就已经超过大多数人了。
比如在“尽量多选出不重叠的区间”这类题里,按结束时间贪婪选择的核心原因是:结束越早,越能腾出后面更多空间。粗看是直觉,细想是一种局部最优导出全局最优的机制。拿到题目先冷静分析,别一上来就套模板,这是我反复踩坑之后最想叮嘱你的一点。
3.4 动态规划入门:状态定义是关键
这套题的压轴题基本都落在动态规划上。动态规划对新手来说最大的门槛不是“转移方程”的代码实现,而是“状态定义”这件玄学般的事。很多时候你看着题解里的转移方程觉得好简单,但关上题解自己从头想,就是死活想不到这一步。
我建议初学者学动态规划时,固定用一套思维框架:“先确定状态表示什么,再决定转移从哪些状态来,最后想清楚初始化和边界条件。”状态定义要尽量贴近问题本身,比如“从前 i 个物品中选,容量为 j 的最大价值”比抽象的“dp[i][j] 表示某个值”要容易理解得多。Nowcoder Girl 2017 里涉及的动态规划,基本不会跳出线性 DP 和背包模型,状态定义清晰之后,转移方程往往是水到渠成的事。
举一个常见的入门模型——路径计数:给定一个矩阵,从左上角走到右下角,每次只能向右或向下,问有多少种不同的走法。状态dp[i][j]定义为“从起点走到 (i, j) 的路径条数”,那么转移就是dp[i][j] = dp[i-1][j] + dp[i][j-1],初始化是dp[0][0] = 1。这个模型虽然简单,但它是很多复杂 DP 的基础,如果你能把这个思路彻底吃透,再去看带障碍物的版本、带权值的版本,会顺很多。
def uniquePaths(m, n): dp = [[0] * n for _ in range(m)] dp[0][0] = 1 for i in range(m): for j in range(n): if i > 0: dp[i][j] += dp[i - 1][j] if j > 0: dp[i][j] += dp[i][j - 1] return dp[m - 1][n - 1]写 DP 题还有一个容易错的地方:初始化。dp[0][0] = 1表示起点本身就有一种走法,但如果是第一行或者第一列的点,只能一直向右或一直向下,这个语义必须对应到代码里。如果你把全部元素初始化为 0,忘了设置第一行和第一列的单调路径,正确率会大打折扣。
3.5 暴力枚举:什么时候可以无脑算
刷算法题的人很容易陷入一个误区:拿到题就想着用什么高级算法,总觉得暴力方案丢人。但我想告诉你一个在 Nowcoder Girl 2017 实战中很实用的经验:在数据范围允许的情况下,暴力枚举是最稳、最不容易出错、也最省脑力的方案。
怎么判断能不能暴力?核心就看数据范围。如果题目给出的数据规模在10^3以内,O(n^2) 的暴力枚举一般没问题;10^3到10^5之间要考虑 O(n log n);超过10^5基本就要用 O(n) 的解法了。这套题的数据范围设计得比较温和,有几道题你用完整的暴力方法也能通过大部分测试点,至少在入门刷题阶段,先保证拿分,再追求优化。
但暴力枚举也不是完全无脑。你要学会“剪枝”:提前判断当前枚举的状态有没有必要继续走,如果已经不符合条件就直接跳过。一个典型的例子是“找所有满足 a + b + c = 0 的三元组”,如果你先固定前两个数,第三个数用哈希表或二分查找去匹配,就能把 O(n^3) 降到 O(n^2)。这种思路在这套题里非常受用,它既保留了暴力的直观性,又兼顾了效率。
写暴力题时我对自己的要求是:在本地先把小数据范围的答案跑出来,再跑一个随机大样例验证一下超时风险。不要只看时间复杂度的理论值,实践中的常数也很重要,尤其是嵌套循环里如果频繁调用复杂函数,很容易被真实数据教做人。
4. 做题过程中的典型问题与排查经验实录
4.1 最常见的几个编译运行错误
刷题过程中遇到编译和运行错误是家常便饭,但很多错误其实完全可以避免。我把这套题中最常出现的几个错误类型整理出来,你在提交前可以照着自查一遍。
数组越界是排名第一的问题。C++ 里数组下标从 0 开始,如果你开了一个长度为 n 的数组,合法下标是 0 到 n-1,访问 dp[n] 就是越界。这类错误在本地运行时可能不报错,也可能报错,但在 Online Judge 上一定是 Runtime Error。解决办法很简单:所有涉及数组下标的循环条件都写i < n而不是i <= n,除非你明确知道要访问到 n。
下面这个表格是我整理的高频问题和排查思路,遇到类似情况可以参考:
| 报错类型 | 常见原因 | 排查思路 |
|---|---|---|
| Compile Error | 头文件缺失、变量重名、语法拼写错误 | 看编译器给出的第一个错误位置,不要被后续报错带偏 |
| Runtime Error | 数组越界、除以零、空指针 | 重点检查数组下标、除法运算、循环边界 |
| Wrong Answer | 思路不对、边界情况漏了、数据格式错 | 拿样例跑通只是开始,构造小数据集手动验证 |
| Time Limit Exceeded | 算法太慢、循环里有重复计算、输入输出太慢 | 看数据范围、检查复杂度、优化 IO 方式 |
4.2 超时的排查流程
在 Online Judge 上,Time Limit Exceeded是仅次于Wrong Answer的常见“送命题”。这套题里虽然整体数据量温和,但如果你用了明显不合适的算法,超时还是会发生。我以前有个坏习惯:一超时就开始乱改代码,resulting in 越改越乱,最后重写一遍反而过了。后来我总结出一套固定的排查流程。
第一步,先看数据范围。题目里一定会给出 n 的最大值,你要根据这个值倒推允许的复杂度。比如 n 最大是 10^5,那 O(n^2) 就很危险,你该考虑 O(n log n) 或 O(n)。第二步,检查自己的代码里有没有多余的操作,常见的是在循环里调用strlen()、substr()这种反复拷贝的库函数,它们会让常数变得很大。第三步,考虑输入输出优化,C++ 加上ios::sync_with_stdio(false);和cin.tie(0);,Java 用BufferedReader代替Scanner,Python 用sys.stdin.read()代替input(),这些改动往往能带来数倍的速度提升。
还有一个容易忽略的点:动态规划的空间和递推顺序也会影响速度。如果你能用一维数组滚动更新,就不必开二维数组;如果你能用迭代,就不使用递归;如果你能用位运算判断某数是否为 2 的幂,就别用循环计数。这些微优化单看不起眼,叠加起来有时能把超时变成稳过。
4.3 边界条件和特殊输入的自查清单
做题到最后关头,真正拉开分数差距的往往不是“会不会做”,而是“做得全不全”。很多解题思路完全正确的人,因为漏掉了一个特殊输入,最终只拿到部分分数。所以我自己总结了一个边界自查清单,每次提交前都会快速过一遍。
清单第一项:最小输入。数组长度、字符串长度、循环次数取最小值时,代码能不能正常工作?比如字符串空串、数组只有一个元素、循环零次。清单第二项:最大输入。数据范围上限时会不会超时、溢出或数组越界?有些题目的答案要用 long long 而不是 int,这一点常被忽略。清单第三项:重复值。如果所有元素都相同,排序去重、计数逻辑还正确吗?比如字符串全是一个字符时的压缩结果。清单第四项:乱序和极端分布。数值最大值最小值都出现时,排序和查找逻辑还稳不稳?
这套检查看起来繁琐,但我告诉你,真正执行下来只要三到五分钟。而这三到五分钟,可能就能帮你挽回一道题二十分。我在 Nowcoder Girl 2017 模拟赛中,就靠这四步检查,在最后十分钟发现了一道字符串题漏掉字符集只有单一字符的情况,改完直接多拿了整道题的分数。
5. 刷完这套题之后怎么办:进阶路线与实在建议
5.1 从这套题出发,下一步怎么走
如果你已经把 Nowcoder Girl 2017 题目集合完整刷了一遍,而且大部分题都能独立做出来,那你可以考虑进入下一阶段了。这套题覆盖的考点虽然广,但深度有限,它更像一个“算法能力体检报告”,让你知道自己目前处在什么水平。
下一步我建议你按两条线走:一条是刷题量线,去更大的题库按主题刷题,比如数组、链表、树、图论、搜索、动态规划各刷二十道,把基础打牢;另一条是竞赛线,去参加一些线上编程比赛,比如 LeetCode 周赛、Codeforces 的 Div. 2 比赛、牛客的练习赛等,在真实的时间压力下检验自己的水平。这两条线不是互斥的,可以并行。
你会发现一个有意思的变化:当你刷题数量积累到一定程度,回头再看这套 Nowcoder Girl 2017 的题,很多原来觉得难啃的题会变得“一眼看穿”,甚至能想出多种不同做法。这就是水平提升的最直观证明。我当年把一套入门题集刷了三四遍,每一遍都有新的领悟,第二遍的重点是优化第一遍的粗糙代码,第三遍是尝试用更简洁的思路重新实现,第四遍是给每道题写题解。这个过程虽然有点强迫症,但对理解深度的帮助非常明显。
5.2 关于女生学编程,说点实在话
既然这套题冠着“Girl”的名字,我忍不住多说几句相关的感受。我见过太多女同学在算法学习上显得信心不足,明明代码写得比很多人都干净,却总觉得自己“逻辑思维不行”“不是这块料”。但实际上,算法能力是一个靠刻意练习就能持续提升的技能,跟你是什么性别没有半点关系。那些在竞赛里拿到奖牌的女生,给男题解的评论区带去结论打铁一般的事实:除了练习量和思考深度,没有任何东西能决定你学会学不会。
如果你因为“女生”这个标签而对这套题目集合感到犹豫,我劝你放下这个包袱,直接开刷。相反,它还提醒我们:编程世界里需要更多不同的声音和视角。技术本身是中性的,比赛题也不会因为你的身份而改变难易程度。
注意:我说这些不是打鸡血。简单来说,如果你真的把 Nowcoder Girl 2017 这套题从头到尾理解透彻了,你完全有资格把下一目标定在更高的赛事或者心仪公司的算法面试上,这份底气和你的性别无关。
5.3 我刷这套题时印象最深的一个习惯
最后分享一个从这套题开始养成、一直用到现在的习惯:每道题做完之后,在代码文件的开头写三行注释——第一行写这道题的考点,第二行写我当时的解题思路,第三行写我踩过的坑。这个习惯一开始只是为了让复盘方便,后来慢慢变成了我的“算法错题本”。
别小看这几行注释,它们的作用远不止是备忘。当你刷到类似题型时,打开原来的代码看一眼,几分钟就能回忆起当时的全部思考过程,比从零开始重新分析快得多。这套题做完,我的注释里积累了很多像“字符串结尾漏判”“贪心没有证明”“DP 初始化忘记边界”这类警句,每一句背后都是一段真实的踩坑经历,比任何教程里写的“注意事项”都来得深刻。
如果你现在刚开始刷这套题,我建议你也试试这个方法。不需要写多么正式的文档,只要自己看得懂就行。刷题不只是为了应付一场笔试或面试,它是训练思维习惯的过程,而这个习惯会在你未来的职业生涯中持续发挥作用。
想想也挺有趣的,当初的一套比赛题,现在变成了不少人算法路上的一个小坐标。我也看到很多年前刷过这套题的人,如今已经在做开源项目、带团队、写技术专栏了。你要是愿意,这就是你刷题记录里的一个新起点;你要是不愿意,它也可以只是查漏补缺的一场练习。反正套用一句大家都在说的话:题不在多,吃透就行。希望这份复盘对你有用。