牛客网Nowcoder Girl 2017刷题总结:核心考点、典型题解与避坑指南
2026/8/30 10:00:54 网站建设 项目流程

最近把牛客网 Nowcoder Girl 2017 这套题目集合重新完整刷了一遍,感触还挺多的。当年第一次看到这套题的时候,我以为就是普通的女生专场编程赛,实际上刷下来发现,它根本不会因为名字看起来“温柔”就降低难度,反而在数据范围、边界条件、思维拐点上埋了不少雷。这套题整体给我的感觉是:基础但不水,典型但不套路。你要是准备春招秋招笔试,或者刷了差不多一两百道题想找个合适的题单进阶,拿这套题来做限时模拟很合适。

它虽然叫 2017 年的题目,但里面的知识点放到现在依然是笔试常客。字符串处理、动态规划、贪心、搜索、二分,这些高频考点基本都覆盖了。更难得的是它题目数量不多不少,用来做一轮集中的专项训练刚刚好。下面我就把这套题里值得展开的考点、典型题目思路、还有我踩过的坑一次性整理出来。

1. 先看这套题的整体情况:难度、题量与考点分布

1.1 难度定位和适合人群

先说难度。整套题不是那种让你怀疑人生的硬核竞赛题,但也不是送分题。它更像是在校招笔试里常见的“中等偏基础”水平,比牛客网上的周赛简单一点,比那些纯入门签到题又明显难一截。

我的判断是,如果你之前已经刷过 LeetCode 或者《剑指Offer》里的常规题,大概完成过 150 道左右,那么这套题的大部分题目你都可以上手。它最好的用法不是用来“挑战”,而是用来检验自己的算法基础到底牢不牢。尤其是那些你觉得“我肯定能写出来”的题,真正动手做的时候才会发现自己在边界条件、代码实现、复杂度预估上还有多少漏洞。

所以适合谁呢?我总结为三类人:第一类是准备参加校招笔试的同学,需要用一套接近真实难度的题来查缺补漏;第二类是刚学完基础算法、想通过成套题目来巩固的人,这套题的考点覆盖非常均匀,不会像某些题库那样偏科;第三类是像我一样想回头复盘老题的人,老题往往比新出的怪题更值得静下心拆解,因为它的出题逻辑更朴素、更贴近基本功。

1.2 高频考点分布

我刷完之后大致统计了一下,这套题目集合涉及的考点可以分成六类,排在最前面的绝对是模拟和字符串处理,第二梯队是动态规划、贪心和搜索,最后还点缀了一些二分答案、差分数组、数学推导类的题目。为了让你一目了然,我整理成一张表:

考点方向出现频率典型特征建议优先级
模拟与实现题目描述长,逻辑直接,考的是细心必刷,签到题常客
字符串处理配合哈希表、双指针、滑动窗口必刷
动态规划中高状态定义直接,转移方程不复杂重点刷
贪心算法需要排序 + 证明贪心正确性重点刷
DFS/BFS 搜索二维矩阵、连通块、迷宫类重点刷
二分答案 / 思维中低题目有“最大值最小”等特征选刷

这套题有个很好的特点:它不会故意把动态规划和贪心揉在一起考得很深,而是更看重你能不能识别题型,然后用最合适的方法在合理时间内解出来。这一点其实非常接近现实中的笔试场景。

1.3 题目结构的小规律

另外我刷的时候发现一个小规律:这套题的前半部分明显偏简单,适合热身;中后段开始上强度,出现了需要推导的题目。这可能是因为比赛本来就希望选手循序渐进地进入状态。平时练习的时候,我建议你按顺序刷,不要跳题,因为这种难度曲线很有价值,它能逼你学会分配精力,而不是一上来就盯着难题死磕。

2. 逐类拆解:几道典型题的完整思路和代码

2.1 字符串与滑动窗口:最长无重复子串

这套题里有一道关于字符串的题目,核心是找最长无重复字符的子串长度。题目本身不绕,但很考验你对双指针和哈希表配合使用的熟练程度。

思路其实很经典:用两个指针维护一个窗口,右指针不断向右扩展,把新字符加入窗口;如果发现窗口内有重复字符,就移动左指针缩小窗口,直到没有重复为止。整个过程只需要遍历一次字符串,时间复杂度 O(n),空间复杂度 O(字符集大小)。

我第一次写这道题的时候,在左指针移动那里犯了个错误。代码如下:

def lengthOfLongestSubstring(s: str) -> int: # 记录字符上一次出现的位置,用字典实现 pos = {} left = 0 ans = 0 for right, ch in enumerate(s): if ch in pos and pos[ch] >= left: # 如果当前字符在窗口内已经出现过,就把左边界跳到上次出现位置的下一个 left = pos[ch] + 1 # 更新当前字符的最新位置 pos[ch] = right ans = max(ans, right - left + 1) return ans

这里有三个细节必须注意。第一,判断是否重复时,不能只看pos[ch]是否存在,因为有些字符可能出现过,但现在已经被排除在窗口之外了,所以要加一个pos[ch] >= left的判断。第二,更新左指针时是跳到pos[ch] + 1,而不是pos[ch],否则重复字符还留在窗口里,结果必然错误。第三,最后更新pos[ch]的位置一定要放在计算答案之前,否则后续判断会出错。

建议你写完代码后,用"abcabcbb""bbbbb"这两个用例跑一遍。前者答案应该是 3,后者答案应该是 1。这两个用例基本能覆盖掉大部分实现错误。

2.2 动态规划:矩阵最短路径和

这套题中有一类典型的动态规划题,比如“从矩阵左上角走到右下角,每次只能向右或向下移动,求路径上的最小数字总和”。这类题在笔试里出现频率极高,因为它考察的是最基础的 DP 推导能力,同时又能延伸出空间优化的问题。

思路是定义一个二维数组dp[i][j],表示从起点走到(i, j)的最小数字总和。因为每一步只能向右或向下,所以走到(i, j)只能来自上方(i-1, j)或者左方(i, j-1),转移方程就是:

dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])

初始化时需要特别注意两点:第一行只能往右走,所以第一行的每个格子只能等于左边格子累加;第一列只能往下走,所以第一列的每个格子只能等于上边格子累加。我见过不少人在这里漏掉初始化,直接用min(dp[i-1][j], dp[i][j-1]),结果第一行第一列越界,代码直接崩。

参考实现:

def minPathSum(grid): if not grid or not grid[0]: return 0 m, n = len(grid), len(grid[0]) dp = [[0] * n for _ in range(m)] dp[0][0] = grid[0][0] # 初始化第一行 for j in range(1, n): dp[0][j] = dp[0][j-1] + grid[0][j] # 初始化第一列 for i in range(1, m): dp[i][0] = dp[i-1][0] + grid[i][0] # 递推填充 for i in range(1, m): for j in range(1, n): dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]) return dp[m-1][n-1]

如果你追求空间优化,可以只用一个一维数组滚动更新,每一行从左往右刷新。再进一步,你甚至可以直接在原矩阵上累加,把空间复杂度降到 O(1)。不过笔试的时候我一般不建议直接改原数组,万一后面还要用原始数据,改完就麻烦了。空间优化这种操作更适合在面试里主动提出来展示思路。

2.3 贪心:活动安排 / 区间调度

这道题在 Nowcoder Girl 2017 里也有类似的变体,核心问题可以理解成:给你若干个活动,每个活动有开始时间和结束时间,同一时间只能参加一个活动,问最多能参加几个。解法非常经典,按结束时间从小到大排序,然后贪心地选择第一个能选的活动,再继续选下一个不冲突的。

贪心为什么是对的呢?因为结束时间越早的活动,给后续活动留下的时间越多,所以按结束时间排序,只要当前活动不冲突就选择它,不会导致全局变差。这个性质需要注意,排序依据是结束时间而不是开始时间。如果你按开始时间排序,结果很可能是错的。

参考代码:

def maxActivities(activities): if not activities: return 0 # 按结束时间排序 activities.sort(key=lambda x: x[1]) count = 1 last_end = activities[0][1] for i in range(1, len(activities)): start, end = activities[i] if start >= last_end: count += 1 last_end = end return count

这里有一个容易搞混的地方:区间判断用start >= last_end还是start > last_end,取决于题目里“同一时间只能参加一个活动”是闭区间还是开区间。如果是活动接续,比如一个活动 3 点结束、另一个 3 点开始,通常是可以连上的,那就要用>=。如果是求区间重叠数量,可能要反过来用<。做题时一定要先看清题意再写代码,别凭经验套模板。

2.4 DFS/BFS:岛屿数量

还有一类搜索题,核心是在二维矩阵里找连通块的数量。比如矩阵里的 1 代表陆地、0 代表海水,1 的上下左右连通在一起算一个岛屿,问总共有多少个岛屿。这类题在牛客笔试里非常常见,套路也固定:遍历每个格子,如果当前格子是 1,就从这个格子开始做一次搜索,把相邻的所有 1 都标记成已访问,然后答案加一。

我第一次写这道题时用的递归 DFS,结果在大数据量下爆栈了。这不是算法逻辑的问题,而是递归层数太多导致的系统栈溢出。后来我改成 BFS,用队列做层序遍历,稳了很多。

参考 BFS 实现:

from collections import deque def numIslands(grid): if not grid or not grid[0]: return 0 m, n = len(grid), len(grid[0]) visited = [[False] * n for _ in range(m)] directions = [(-1,0), (1,0), (0,-1), (0,1)] ans = 0 for i in range(m): for j in range(n): if grid[i][j] == '1' and not visited[i][j]: ans += 1 # BFS 访问整个岛屿 q = deque() q.append((i, j)) visited[i][j] = True while q: x, y = q.popleft() for dx, dy in directions: nx, ny = x + dx, y + dy if 0 <= nx < m and 0 <= ny < n and not visited[nx][ny] and grid[nx][ny] == '1': visited[nx][ny] = True q.append((nx, ny)) return ans

这里最容易踩的坑是:在从队列弹出节点时才标记visited,而不是在入队时标记。那样会导致同一个节点被多次入队,严重时可能超时或者死循环。正确做法是,在决定把邻居加入队列的那一刻就把它标记为已访问。还有一个细节是方向数组的写法,用dx, dy的四个组合比分别写四个 if 清晰得多,也不容易漏。

2.5 二分答案:钢管切割

这套题里还有一类让我印象深刻的题目,表面上看起来是模拟,实际上要二分。典型例子是“给你 n 根钢管各自的长度,要切出长度相同且总长度不小于 m 的一批短钢管,问每段最长能切多长”。这种题如果你直接去模拟切割过程会很痛苦,因为每段长度不确定,很难直接算。

正确思路是二分答案:二分每一段的目标长度mid,然后检查把所有钢管按mid长度切割,总共能切出多少段,看是否达到要求。如果切出来的总段数够多,说明当前长度可行,可以尝试更大;如果不够,就说明长度太大,需要缩小。

参考代码:

def maxCutLen(sticks, need): def check(length): if length == 0: return True total = 0 for s in sticks: total += s // length return total >= need left, right = 1, max(sticks) ans = 0 while left <= right: mid = (left + right) // 2 if check(mid): ans = mid left = mid + 1 else: right = mid - 1 return ans

二分答案题目有一个很明显的识别特征:题目里出现“最大能是多少”“最少要多少”这种字眼,而且直接算很难,但给你一个固定答案后你很容易验证它是否可行,那就大概率要用二分。这道题还有一个变体,比如每根钢管切出来的总段数不限,但要求每段长度相同,如果题目改成问“最多能切成多少段”,那就退化成贪心,直接把每根长度除以段数累加即可。这两种题型别搞混。

3. 刷题时最容易被坑的细节

3.1 输入输出:多组数据、空行和末尾空格

牛客系的在线评测和 LeetCode 有一个很大的区别:很多题目要求你自己处理输入输出,而且可能是多组数据。Nowcoder Girl 2017 这套题我也遇到了类似的情况。如果题目说输入包含多组测试数据,每组两行,那你的代码必须放在循环里,一直读到 EOF 为止,而不是只处理一组就结束。

第一次刷这种题的人经常会犯“只处理一组数据”的错,本地测试看着没问题,一提交就 WA。解决方法是写代码之前先看输入描述,凡是出现“多组”“多行”“EOF”字样的,一律用循环包起来。比如 Python 里可以这样读:

import sys for line in sys.stdin: # 处理一行,注意 strip 掉末尾换行 arr = list(map(int, line.strip().split())) # 继续处理

这里还有个藏得很深的坑:有时候行尾有多余空格,split()能自动处理,但如果你用split(' ')手动切分,空字符串会让你代码出错。强烈建议用split()而不是split(' ')。输出的时候,要注意行尾有没有要求空格,如果要求输出的多个数字用空格分隔,建议先塞进列表,最后再join,不要边循环边 print,否则很容易多打一个空格被判定格式错误。

3.2 边界条件和数组越界

刷这套题的时候,我被一个边界条件坑过:有一道题要求处理二维矩阵,但矩阵可能为空,也可能只有一行或只有一列。我的代码里直接取了grid[0][0],结果矩阵为空的时候直接数组越界。这类问题只要你习惯性地在函数开头加一个空值判断就能解决。

我整理了一个边界条件自查清单,每次写完代码照着看一眼:

  • 数组为空,或者数组里的元素为空,是否单独处理?
  • 字符串长度是 0 或 1 时,代码还能跑吗?
  • 循环里有没有用到i+1i-1,会不会越界?
  • 题目给的数字范围是否有负数?负数和正数处理逻辑一样吗?
  • 如果有取模操作,遇到负数时结果是否符合预期?

这套题里的模拟题特别多,模拟题的隐藏坑基本都藏在细节里。比如某道题要求从某一天开始往后数,天数可能会跨年,如果你直接用月份天数去减,很可能忽略 2 月闰年的特殊情况。边界条件这种事,真不是靠眼力就能看出来的,必须靠测试用例去逼出来。

3.3 数据范围决定算法选型

刷题最重要的一个能力就是看到数据范围立刻反应出应该用什么复杂度的算法。这套题虽然不像 ACM 那样数据量大到离谱,但如果你不看数据范围直接写 O(n^2) 暴力,照样可能超时。

通常的经验是这样的:如果n <= 1000,O(n^2) 可以接受;如果n <= 10^5,必须 O(n log n) 或 O(n);如果n <= 10^8,基本上只能 O(n) 并且常数要小,或者是 O(log n) 级别的二分。你在动手写代码之前,先看一眼输入数据范围,心里有个复杂度预算,能省下很多不必要的超时调试。

另外要提一下语言选择的问题。牛客网支持 Java、C++、Python 等语言,Python 写起来最快,但在某些大常数场景下确实可能 TLE。如果你选 Python,建议多用内置函数和切片操作,少写几层嵌套循环;如果发现超时,除了优化算法,还可以考虑把递归改成迭代,把循环里的重复计算提到循环外。实在不行再换 C++ 或 Java,别在 Python 一条路上死磕。

3.4 取模、浮点和整数溢出

这套题里有一道涉及计数的题,要求结果对一个大数取模。取模本身不难,但有几个细节需要注意。第一,中间结果要边算边取模,不能等到最后再对结果取一次模,因为中间过程可能早就溢出了。C++ 和 Java 尤其要小心,Python 虽然整数不限制长度,但大整数运算会变慢,因此也要适度取模。

第二,涉及小数的时候,尽量避开浮点数。比如题目可以转化成整数时,就把它转化成整数;如果一定要用浮点,要注意精度问题,判断相等时不要用==,而是用abs(a - b) < 1e-9。有时候题目会在数据里埋 0.1 这种精度陷阱,不小心就 WA。别嫌我啰嗦,这一点在笔试里真的非常常见。

4. 比赛现场的时间分配与调试经验

4.1 做题顺序:先签到,再热身,再上强度

Nowcoder Girl 2017 毕竟是场比赛,比赛和平时的最大区别就是限时。刷这套题的时候,我强烈建议你也限时做,不要一道题磨一个小时。按照这套题的难度分布,我个人的做题顺序是:先做模拟题和字符串题,这类题思维难度低,先拿分,稳住心态;再做动态规划和贪心,因为这类题想清楚之后代码往往很短;最后做搜索和二分答案题,这类题有时需要多写一点代码,而且容易出边界问题,放到最后比较从容。

我见过太多人一上来就死磕最后几道难题,结果前面送分题没时间做,心态也崩了。比赛不是“证明自己可以做难题”,而是“在有限时间内拿最多的分”。

4.2 先写暴力保底,再想优化

如果你遇到一道题一眼看不出正解,我的经验是先写一个暴力版本。这个暴力版本的作用有两个:第一,它至少能帮你拿一部分分数;第二,它可以在你写完正解之后作为数据校验器,用随机数据对比正解和暴力的输出,如果一致,基本说明正解逻辑没问题。

这套题里有一道搜索题,我一开始没想到剪枝,先用纯 DFS 写了个暴力版本,然后小数据测试正确。后来我加上记忆化搜索,再拿同样的测试用例去对比,发现输出一致,才放心提交。这个技巧在真实比赛里非常实用,而且写暴力还不容易把思路搞乱。

4.3 调试技巧:打印关键中间值

调试的时候不要漫无目的地加 print。我的习惯是,如果某个程序结果不对,先在关键位置打印出中间变量。比如矩阵 DP 题目,打印出整个 dp 矩阵,一眼就能看出是哪一行哪一列初始化错了;滑动窗口的题目,打印 left、right、当前窗口计数,就能定位是边界移动逻辑有问题还是计数更新有问题。

当你定位到某个函数或者某段逻辑有问题后,再针对性地构造小用例去验证。比如字符串题目,先用长度 1 和长度 2 的输入测试;矩阵题目,先用 1 行 2 列、2 行 1 列这类极端形状测试。这类小用例跑得飞快,比在大样例里大海捞针有效率得多。

4.4 不要死磕一道题

我还想强调一句,不止一个朋友在刷这套题时栽在“死磕”上。一道题想了 40 分钟没思路,最明智的选择是先把这道题标记为待做,继续做后面的题。比赛结束后,再把这道题当成专题去研究,整理到自己的笔记里。这样既能保证整体分数,又不会因为某道题浪费太多时间。

限时训练的意义就在这里:它逼你在“做不出来”和“放弃”之间做权衡。真实笔试中也会遇到完全没思路的题,宁可战略性放弃,也不能让心态塌掉。

5. 常见问题速查与避坑清单

我把刷这套题过程中遇到的典型问题整理成一张表,方便你回头对照自查:

常见问题可能原因解决办法
本地测试没问题,提交后 WA多组数据没有循环读取用 while/for 循环读到 EOF
数组越界报错没有考虑空数组或单行单列代码开头加空值判断
超时 TLE复杂度太高或 Python 常数太大优化算法,考虑改用迭代或换语言
结果偏大溢出中间结果没有及时取模每一步运算后都取模
输出格式错误多空格、少空格、多了换行用列表收集结果后用 join 输出
递归深度导致栈溢出DFS 递归层数太多改 BFS 或显式栈

除了这张表,我再额外分享一个做事习惯。我刷完这套题之后,把每道题按照“考点、错误类型、解决思路”整理成了一个小笔记。比如“字符串题:窗口字符计数逻辑错;DP 题:第一行第一列初始化漏了;搜索题:visited 入队时才标记导致重复入队”。考试前拿出来扫一遍,比临时翻代码效率高得多。

还有一个做题时的小技巧:在本地写题时,建立一个专门放测试用例的文件,把边界用例都丢进去,比如空字符串、长度为 1 的数组、全是 1 的矩阵、全是 0 的矩阵、最大值和最小值相邻的数据。这样每次改完代码就可以一键跑完所有用例,不用一遍遍手敲输入。

这套题我刷了三遍,每一遍都有不同的收获。第一遍是老老实实按题号顺序做,遇到不会的死磕很久,最后发现很多题其实就是题型识别问题,识别对了之后解法并不复杂。第二遍我限时模拟,发现自己的做题顺序有很大问题,太容易在前面浪费大量时间。第三遍我是冲着“整理复盘”去的,把每一道题都拆开,用不同解法写了几遍,比如 DP 题尝试空间优化,搜索题同时写 DFS 和 BFS,字符串题尝试双指针其他变体。到了这一步,才算真正把题目消化掉。

最后再分享一个小技巧:如果你用这套题自测,可以给自己定一个比笔试更短的时间,比如总共 90 分钟的题量压缩到 75 分钟。高压之下最容易暴露问题,平时多暴露一点,正式上场就会稳很多。这套题不适合只刷一遍就丢,把它当成检测基本功的基准题库,每隔一段时间回来重新刷一次,你会明显感受到自己在算法思维和代码实现上的进步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询