猿辅导2020校招笔试真题解析:字符串处理、动态规划与拓扑排序全攻略 1. 写在前面猿辅导2020校招笔试二到底考什么提到猿辅导2020校招笔试二这个标题经历过那批秋招的同学应该都有印象。在线教育赛道在2020年正处于高速扩张期猿辅导作为头部玩家技术岗的简历池子相当深笔试筛选力度也大。笔试二对应的是同一批校招里的第二套题整体风格和第一套一脉相承都是典型的互联网公司算法笔试题型限时、ACM模式、考察数据结构和算法基本功。这篇文章就围绕这套笔试题展开聊聊在线教育类互联网公司笔试题的考点分布、典型解题思路以及我当时准备和实战过程中的一些经验。不管你是正在准备校招的应届生还是打算跳槽到在线教育行业的技术人只要目标是算法笔试这一关这篇文章都能给你一些直接能用的东西。先交代一下我的背景免得大家觉得我在空谈。我是2020届校招生当年投过猿辅导的后端开发岗笔试、面试一路走下来最后也拿到了offer。后面两年帮学弟学妹改简历、做模拟面试又陆陆续续接触了不少在线教育公司的笔试题。所以对这类笔试的出题偏好、难度梯度和常见坑位算是摸得比较透。这套题的核心关键词就三个字符串处理、动态规划、搜索与贪心。如果你把这些题型吃透了不光猿辅导其他在线教育大厂的笔试也能覆盖七八成。下面我逐个拆。2. 笔试题型与考点分布的整体判断2.1 猿辅导这类在线教育公司偏好什么题型很多人拿到题目第一反应是刷LeetCode这个方向没错但针对性不够强。互联网教育公司和做电商、做社交的公司在笔试出题上有一个很大的区别业务场景会渗透到题目背景里。比如直播课排课、老师学生匹配、题库去重、作业批改这类场景非常容易包装成算法题。你看起来是一道字符串题本质上是处理课程ID看起来是一道区间题本质上是在做教室调度。我在刷题和实际笔试中总结下来猿辅导2020年这批笔试题主要集中在以下几类题型出现频率难度系数典型包装场景字符串处理与模拟高中等题目ID校验、内容关键字提取线性动态规划高中高任务分配、最优课表组合区间与贪心中高中等教室排课、直播时间段安排搜索BFS/DFS中中等偏上知识点关联、错题本聚类二分答案中高服务器资源分配、并发阈值判断这个分布不是巧合。在线教育平台的业务核心是“老师—内容—学生”三端的匹配与调度天然需要处理大量带约束条件的优化问题。所以在笔试里动态规划和贪心永远是重头戏因为这两种算法最能模拟真实业务中的决策过程。而字符串处理则是所有业务的基础几乎每个系统都离不开。2.2 难度梯度设计送分题、核心题、压轴题一套完整的校招笔试题通常会在难度上做明显的梯度区分。猿辅导2020校招笔试二的题目结构大概是这样的第一道送分题。通常是字符串处理或简单模拟上手快但边界条件多。这道题的目的不是筛选而是让大部分考生都能答出来保证考试体验。但注意送分不代表可以粗心恰恰是这道题最容易因为边界条件丢分。第二道核心题。大多数是一道需要思考的动态规划或者贪心用来区分“会刷题”和“真理解”的考生。这套题的第二题我记得非常清楚是一道带约束的区间选择问题背景是直播课时间段安排核心模型是加权区间调度。如果你没有接触过这类DP模型现场推会耗费大量时间。第三道压轴题。通常是搜索BFS/DFS或者二分答案。这道题用来筛出真正有竞赛思维或者算法功底扎实的候选人。难度不在于算法本身而在于如何把问题抽象成可求解的模型。这个难度梯度其实是互联网大厂校招笔试的通用设计逻辑。不要指望每道题都AC策略性地拿下前两道半就已经能超过绝大多数人了。3. 字符串处理与模拟最容易被忽视的丢分重灾区3.1 为什么要重视字符串题别看字符串处理看起来简单实际上在校招笔试里字符串题是失分率最高的一类。原因很简单大家觉得简单就会掉以轻心。猿辅导2020校招笔试二里第一道题就是一个典型的字符串处理题目背景是课程ID格式校验 去重排序考察的知识点是字符串拆分、规则匹配和排序。我记得那道题的核心需求是这样的输入一批课程ID每个ID由字母和数字组成需要按规则校验合法格式然后对合法ID做去重并按字典序输出。看起来完全不难但实际考场上通过率并不高。原因在于各类边界条件比如空字符串、非法字符、重复ID、大小写混用、ID长度超限。这些细节在LeetCode上刷题时不会特别留意但在ACM模式的笔试里每一个边界case都是隐藏的炸弹。3.2 字符串题的通用解题套路我后来总结了一套字符串题的通用处理流程基本能应对这类笔试先确定输入格式。ACM模式下输入可能是多行每行可能包含字符串和数字混合先用split或者Scanner按行读取确认每个字段的类型。写规则校验函数。尽量把校验逻辑单独拆成函数不要和主流程混在一起否则调试的时候非常痛苦。把所有边界case列全。空字符串、全非法字符、重复项、大小写、首尾空格每个都要考虑到。用合适的数据结构去重。HashSet是首选如果要求排序再用TreeSet或者转List排序。比如一个典型的课程ID校验函数伪代码大概是这样的boolean isValidCourseId(String id) { if (id null || id.length() 0 || id.length() 10) { return false; } // 规则首字符必须是字母后续字符是字母或数字 if (!Character.isLetter(id.charAt(0))) { return false; } for (int i 1; i id.length(); i) { char c id.charAt(i); if (!Character.isLetterOrDigit(c)) { return false; } } return true; }这种题目没有技术难点纯粹考验你写代码的严谨度。我当时在考场上的经验是这类题写完后不要急着提交花两分钟手推几个测试用例尤其是空串、单字符、全非法字符这几种极端情况。两分钟的时间投入可能帮你挽回10%的通过率。3.3 字符串处理的高频考点清单结合猿辅导和其他在线教育公司的笔试题我把字符串处理的高频考点整理成了一张清单字符串拆分与拼接split、StringBuilder字符频率统计HashMap计数滑动窗口求解子串问题最长无重复子串、最小覆盖子串前缀与后缀匹配KMP、字符串哈希正则表达式匹配笔试中较少但偶尔会出现括号匹配与合法性校验栈字符串与数字互转大数加法场景如果你时间有限优先掌握前四个就够用了。后面几个属于进阶内容如果目标公司不是特别卷的大厂出现概率不高。4. 动态规划在线教育笔试的核心主菜4.1 动态规划怎么成了高频题型动态规划在猿辅导2020校招笔试二里出现得非常自然因为在线教育业务里到处都是“带约束的优化问题”。说到这里我想起这套题里的第二道核心题它用的是“直播课时间段安排”的背景。题目大意是系统要安排若干节直播课每节课有开始时间、结束时间和预计听课人数教室只有一个问怎么安排能让总听课人数最大。本质上这就是一个加权区间调度问题标准解法是区间DP 二分查找优化。这类题的核心难点在于状态定义。很多同学一看到区间问题就想贪心但加权区间调度和普通区间调度最大的区别是每个区间带权重按结束时间排序后的贪心不一定正确。这里必须用DP否则一定会出错。4.2 加权区间调度的状态设计与转移我先直接给出状态定义和转移方程再解释为什么这么设计。首先把所有课程按结束时间从小到大排序用dp[i]表示前i个课程中能获得的最大听课人数。转移方程如下dp[i] max(dp[i-1], dp[p[i]] value[i])其中p[i]表示和第i个课程不冲突的、结束时间小于第i个课程开始时间的最靠后的那个课程的编号。这个p[i]可以通过二分查找快速求得。这个状态定义的巧妙之处在于dp[i-1]代表不选第i个课程的情况dp[p[i]] value[i]代表选了第i个课程的情况。因为p[i]是最后一个和第i个课程不冲突的课程所以选了第i个课程之后前面只能选到p[i]为止的最优解。核心代码如下// courses: [start, end, value] Arrays.sort(courses, (a, b) - a[1] - b[1]); int n courses.length; int[] dp new int[n 1]; int[] endTime new int[n]; for (int i 0; i n; i) { endTime[i] courses[i][1]; } for (int i 1; i n; i) { int start courses[i-1][0]; int idx binarySearch(endTime, start) 1; dp[i] Math.max(dp[i-1], dp[idx] courses[i-1][2]); }这里有一个容易出错的地方dp[i]的下标从1开始而courses数组的下标从0开始对应关系一定要理清否则很容易出现数组越界或者取值错位。我在考场上一开始也在这里翻过车后来调整成courses[i-1]的写法才理清楚。4.3 从区间DP到其他高频DP模型加权区间调度只是动态规划里的一个模型。从猿辅导2020校招笔试二的整套题来看我还总结了几个高频DP模型按出现概率排序如下第一是线性DP。包括LIS最长递增子序列、LCS最长公共子序列以及它们的变体。这类题的共同特点是状态定义很直接难点在转移方程的优化上。比如LIS的O(n log n)解法用到了贪心二分这个优化技巧在线教育公司的笔试题中出现频率很高。第二是背包类DP。0-1背包、完全背包、多重背包尤其是0-1背包的一维滚动数组优化。业务场景通常是“预算有限如何选择课程组合”或“服务器资源有限如何分配离线任务”。0-1背包的一维优化内层循环必须倒序遍历这个知识点几乎是必考的。第三是区间DP。典型场景是矩阵连乘、石子合并、回文串分割。在线教育笔试中区间DP通常不会单独出题而是嵌套在字符串处理或者搜索问题里。比如“把一段错题文本分成多段每段有独立评分求最小代价”这类题目。第四是状态压缩DP。出现概率不高但如果题目里出现“最多不超过10个元素”的集合遍历大概率就是状态压缩DP。这类题难度较高属于压轴题的范围建议有精力再攻。4.4 动态规划的通用debug方法参加笔试时最怕的不是不会写DP而是写出了DP但结果不对又查不出问题。我分享几个实战中特别管用的debug方法第一小规模暴力对拍。在本地暴力写一个递归版本然后随机生成小规模测试数据对比DP结果和暴力结果。笔试虽然不能这么做但平时训练时这招效率极高能把状态转移方程的疏漏快速暴露出来。第二打印dp表。把dp数组打印出来逐个位置检查是否符合预期。很多时候状态转移写错了看dp表一眼就能发现规律不对劲。我在训练时养成了“写完DP先打表”的习惯省掉了大量瞎猜的时间。第三重点检查边界。dp[0]和dp[n]这两个端点是最容易出问题的位置。比如上面提到的加权区间调度idx可能为-1此时要确保数组下标不为负。5. 搜索、贪心与二分答案压轴题的常见组合5.1 BFS与DFS的实战选择猿辅导2020校招笔试二的压轴题我记得很清楚是一道与“知识点关联图”相关的题目。背景是知识图谱中的节点关联给定若干知识点和它们之间的依赖关系需要判断是否存在循环依赖。这个问题本质上就是拓扑排序解决办法是BFS。从那道题往后我总结了一条经验在线教育公司的笔试压轴题很少考纯裸的搜索通常会在图/树的骨架上套一层业务包装。比如知识点之间的依赖判断 - 拓扑排序错题本中相似题聚簇 - 并查集或BFS连通分量直播课推荐路径 - 最短路径变体Dijkstra或BFS应对方法也很简单把经典搜索模板吃透。BFS的模板我用得很顺手贴在下面给大家参考from collections import deque def bfs(graph, start): visited set() queue deque([start]) visited.add(start) while queue: node queue.popleft() # 处理当前节点 for neighbor in graph[node]: if neighbor not in visited: visited.add(neighbor) queue.append(neighbor)BFS适合求最短路径、层级遍历、拓扑排序DFS适合遍历所有可能路径、回溯剪枝。笔试中优先用BFS因为BFS逻辑清晰、不容易写出死循环而且很多场景下BFS的效率更优。5.2 贪心策略与排序预处理贪心算法在笔试中的出场率比想象中高。猿辅导这套题虽然没有直接把贪心作为压轴但在第二道核心题的前置步骤里如果用了贪心排序能大大简化后续DP的复杂度。贪心题的核心能力就是证明贪心选择性质的正确性。很多同学做贪心题靠感觉这个习惯在笔试中很危险。因为笔试的测试用例有限某个测试点恰好被你猜对并不能说明你的算法正确。我看过太多同学信心满满提交代码结果只过了30%的用例这种挫败感特别打击人。那怎么办我的建议是用反例验证贪心的正确性。写出贪心方案后立刻尝试构造一个反例。如果短时间内构造不出来再花一分钟考虑其他解法。如果构造出来了就说明这道题不是纯贪心可能是DP贪心或者二分贪心。5.3 二分答案压轴题的最后一公里二分答案在猿辅导2020校招笔试二里虽然没有单独出成一道题但它是解决很多DP和搜索题的底层工具。我在第二节提到的加权区间调度里二分查找就是关键的优化手段。二分答案的一般流程是确定答案的上下界然后在范围内二分对每个猜测值用check函数验证是否可行。check函数通常用贪心或模拟实现。这个过程听起来简单但坑很多。比如边界条件写错、up和down更新逻辑反了就会死循环或者漏答案。我分享一下我一直在用的二分模板这个模板经过大量验证几乎没有翻过车int left 0, right Integer.MAX_VALUE; while (left right) { int mid left (right - left) / 2; if (check(mid)) { right mid; } else { left mid 1; } } return left;注意left (right - left) / 2这种写法能防止leftright溢出的问题。笔试环境里虽然很少考溢出但养成好习惯总没错。6. 综合实战一套模拟笔试题的完整推演这一节我准备带大家手把手走一遍完整的笔试流程体验一下拿到题目后从读题到AC的全过程。这套题是我根据猿辅导2020校招笔试二的题型风格模拟出来的核心考点和原题高度吻合。6.1 题目一课程ID合法性统计题目描述给定一组课程ID每个ID由字母和数字构成首字符必须是字母长度不超过10。统计合法ID中不同字符的数量之和。输入为多行每行一个ID读到EOF结束。解题思路这是一道送分题只需要按行读取字符串校验合法性然后用HashSet统计不同字符数量。关键细节是读入多行直到EOF的写法。Java里用while (sc.hasNextLine())Python里用sys.stdin.read().splitlines()。校验逻辑参考我在3.2节里贴的isValidCourseId方法。写完校验后对每个合法ID把它转成字符数组全部加入一个全局的HashSet最后输出set.size()即可。这道题看似简单但我见过很多人的错误都是在对“不同字符数量”的理解上。有的人把每个ID的内部字符去重后再求和得到的结果和全局去重完全不同。题目描述如果写的是“所有合法ID中不同字符的数量之和”一定要逐字逐句地读清楚。6.2 题目二直播课最大收益题目描述有一间直播间给出若干课程的时间段和收益不能同时上两节课求最大总收益。输入格式第一行为课程数n接下来n行每行三个整数start、end、value。解题思路这就是我在4.2节里讲的加权区间调度问题直接按那个方案实现就行。这一题的关键步骤就两步第一步按end排序第二步构建dp数组。排序时注意用lambda比较器要用a[1] - b[1]而不是a[0] - b[0]。我在这里栽过一次跟头因为题目背景是直播课直觉上会按开课时间排序但加权区间调度必须按结束时间排序这是算法的前置条件。另外还要注意value的数据范围。如果value很大记得用long而不是int来存储dp数组否则结果会溢出。2020年很多企业的笔试题都在这个细节上挖了坑测试用例专门准备了大收益数据用int的人直接爆掉。6.3 题目三知识点调度与循环依赖检测题目描述课程之间有依赖关系比如“学完基础语法才能学高级语法”。给定总节点数和依赖边判断是否存在循环依赖并输出任一合法学习顺序。如果存在环输出-1。解题思路标准的拓扑排序。计算每个节点的入度维护一个入度为0的队列逐个弹出并减少后继节点的入度。如果最终弹出的节点数等于总节点数则不存在环否则存在环。from collections import deque def topo_sort(n, edges): graph [[] for _ in range(n)] indeg [0] * n for u, v in edges: graph[u].append(v) indeg[v] 1 queue deque([i for i in range(n) if indeg[i] 0]) result [] while queue: node queue.popleft() result.append(node) for nxt in graph[node]: indeg[nxt] - 1 if indeg[nxt] 0: queue.append(nxt) if len(result) ! n: return -1 return result拓扑排序的坑主要在两个方面一个是有多个入度为0的节点时输出顺序不唯一判题系统一般不会强校验具体顺序但如果你输出的顺序不满足依赖关系那一定是写错了另一个是边数可能为0的情况此时所有节点都是孤立节点直接按索引输出即可。这道题的考察点不仅是算法本身更是对图论基础概念的掌握程度。如果拓扑排序、入度、依赖关系这些概念不熟考场上一紧张很容易写成DFS暴力判断环复杂度爆炸不说代码量还特别大。6.4 时间分配与做题顺序建议做完这套模拟题我再聊聊考场上的节奏把控。我个人的习惯是拿到题目后先花3到5分钟通读全部题目把每道题的题型和难度做一个初步判断然后确定做题顺序。我的策略是严格按题目顺序来但并不推荐所有人这样。如果你一眼看出第一题是字符串处理而恰好你擅长DP可以先做第二题。但有一个原则不能变先把送分题拿下再啃硬骨头。因为送分题的通过率直接决定你的基本盘压轴题AC不了最多只是少拿分送分题翻车损失更大。时间分配上一场120分钟的笔试我的分配比例大致是送分题20-30分钟核心题35-40分钟压轴题30分钟最后留10-15分钟做边界测试和复查。如果压轴题30分钟还没有清晰思路果断放弃把时间用到核心题和遍历复查上这比死磕一道题划算得多。7. 考场避坑指南与高频错误清单7.1 输入输出处理ACM模式的老生常谈猿辅导2020校招笔试二用的平台是牛客网或赛码网输入输出都是标准输入输出既不是LeetCode那样的函数调用也不是力扣内部的核心代码模式。很多习惯了LeetCode刷题的同学第一次接触ACM模式会非常不适应。核心点是你要自己写IO代码。不要在代码里写文件路径不要用Scanner读取图形界面的输入所有数据都从stdin读所有结果都用stdout输出。Java里可以用BufferedReader包装InputStreamReader来提升读取效率Python里用sys.stdin.read()一次性读取所有内容再按行解析。输出时如果是多组结果每行一个结果确保格式和题目要求完全一致。我在笔试中见过几个因为IO问题翻车的案例。有人多输出了一个空格答案被判错误有人使用了System.out.println()输出调试信息导致输出流混入调试内容全盘皆输。这些不是算法问题是操作习惯问题千万要重视。7.2 常见错误类型与快速排查笔试中常见的报错类型我整理了一个速查表报错类型可能原因排查方法编译错误语法错误、导入缺失、类名错误检查主类名必须为MainJava答案错误算法思路或边界条件有问题手推测试用例重点看边界运行超时时间复杂度太高或死循环优化算法检查循环终止条件内存超限数组开得过大或递归栈溢出缩小数组范围改循环为迭代Java的主类名必须是MainPython不需要类名这是ACM模式最容易踩的坑之一。有的同学在自己本地IDE里用public class Solution跑通了粘贴到牛客上编译报错改一行类名就过了。这种低级失误非常可惜。7.3 边界条件与极端值测试写完代码后我会做一轮快速测试覆盖以下场景最小规模n1甚至n0最大规模n100000或者题目给定的上限全相同值所有输入完全一致全逆序或全正序包含负数和零字符串为空或只有一个字符这套测试方法不是论文里的理论而是我在一次次笔试和面试中总结出来的。测试通过的标准不是“不报错”而是“输出符合预期”。比如二分查找返回的索引是否正确、dp数组的初始值是否合理都要亲自验证。7.4 心态与体力管理最后说一个很多人忽略的点笔试拼的不只是算法还有心态。在线教育公司的校招笔试通常在晚上进行持续90到120分钟全程盯着屏幕对体力和专注度都是考验。我建议大家笔试前半小时不要做剧烈运动不要喝太多咖啡保持大脑清醒。如果遇到卡了20分钟没思路的题果断跳过。我当时考猿辅导的时候第二道DP题目卡了大概15分钟发现状态转移写不对立刻转去做第三题的拓扑排序等拓扑写完再回过头来重新推导DP反而思路打开了。这种“绕开硬骨头、先吃软柿子”的做法能让你在有限时间内拿到最多分数。8. 复盘方法论如何把一套笔试题的价值榨干笔试结束不代表这个题目就作废了。我从带过的学弟学妹身上发现一个规律那些进步最快的人不是刷题最多的人而是复盘最细的人。拿到一套笔试题后无论AC几道我都会做三件事。第一把每道题重新做2到3遍。第一遍按自己的思路完整AC第二遍把暴力解优化到最优解第三遍尝试用不同算法实现同一道题。比如加权区间调度DP可以做贪心优先队列也可以做多解对比能加深对算法适用场景的理解。第二把同类题型整理到一起。比如把“区间DP”的题目归类把“拓扑排序”的题目归类每类题整理出一个模板和一份易错点笔记。下次再遇到同类题直接套模板不用从零开始搭建思路。第三限时重做。把AC过的题目放两个星期后再拿出来限定比原题时间短的情况下重新做一遍。很多人当时AC的题过一个月就写不出来了。第二次能在更短时间内写出来才说明这个知识点真正内化成你自己的了。关于这套笔试题的难度我最后再说一句。它的整体难度在当年校招笔试里属于中等偏上核心原因不是题目本身多难而是题量适中、每道题都有区分度且业务场景包装得很自然。如果你能把字符串处理、动态规划、拓扑排序这三座大山翻过去应对猿辅导乃至其他头部在线教育公司的笔试胜率会非常高。我身边的同学里凡是提前把加权区间调度和拓扑排序模板吃得透透的笔试基本都过了。这个经验放到今天依然适用。