映客2020春招算法E卷复盘:KMP、区间DP与笔试策略

发布时间:2026/8/31 15:56:23
映客2020春招算法E卷复盘:KMP、区间DP与笔试策略 春招笔试这回事做过算法的同学应该都有体会刷题时觉得自己无所不能拿到卷子才发现三个小时根本不够用。映客2020春招算法E卷就是典型的题量不大但每道都磨人的卷子。我印象比较深的是这套卷子没有太多偏题怪题但非常考验基本功的扎实程度——数据结构、经典算法、机器学习基础原理都覆盖得挺全面。今天这篇不打算单纯对答案我想从这套卷子到底在考察什么出发把每一类题目的解题思路、容易踩的坑、以及做题时的策略选择都拆开聊聊不管你是正在准备春招的学生还是想跳槽的工程师应该都能从中找到点有用的东西。1. 拿到E卷的前半小时题型分布与时间分配策略1.1 卷面整体情况四道编程题加八道简答题E卷的结构其实不算复杂。我拿到卷子先花了五分钟把整张卷子翻了一遍大致情况是四道编程题八道简答题总时长三个小时。这个配置最大的特点就是——编程题每道都值二十分左右简答题则覆盖了从数据结构到机器学习再到工程实践的多个方向。这里要提醒一个非常关键的策略先花五分钟通览全卷永远比直接埋头做第一题更划算。我当时扫完一眼就判断出四道编程题里有一道是KMP相关的字符串题两道是偏动态规划和贪心的题目还有一道是图论的变体。简答题里面有粒子群算法原理、KL散度和ELBO的关系、排序算法的稳定性对比、二分图匹配这类经典问题。五分钟的通览能让你直接建立一张得分地图哪些题是你闭着眼都能拿分的哪些题是需要在草稿纸上推半天才敢动笔的。我一般在三个小时里会做这样的分配时间段任务目标前5分钟通览全卷标记难易建立答题顺序5分钟-90分钟先把有把握的编程题AC掉稳住基本盘90分钟-120分钟处理简答题中熟悉的知识点快速拿分120分钟-170分钟啃剩余的难题争取得分最后10分钟检查输入输出边界避免无谓失分1.2 不同题型的时间投入比以及为什么不能平均用力很多人做题的时候有个坏习惯——按顺序一道一道来遇到卡住的地方就死磕。这个习惯在笔试里特别致命。E卷的编程题难度并不是线性递增的甚至第二题可能比第四题还难。你如果被第二题卡了四十分钟后面能拿分的题反而没时间做这才是最亏的。我的建议是编程题如果十五分钟还没有清晰的思路立刻跳过去做下一道。这不代表放弃而是让大脑在后台自动运行。很多时候你做完后面那道题回过头来看前面这道卡住的题思路反而通了。这在心理学上叫酝酿效应在算法笔试里真的特别管用。简答题的时间投入更要讲究性价比。八道简答题每一道大概值五到八分你不需要每道都写满只需要每道都写出核心答案、对一半以上就行。遇到不会的简答题千万不要留空白写点相关的概念推导、画个流程图、贴个公式阅卷老师多少会给点辛苦分。我见过太多人在简答题上留白结果总分就差那么一两分进面试。2. KMP的next数组让不少人翻车的第一道送分题2.1 题目原貌模式串 pabacaba 的next数组计算先说结论这道题出现在E卷的编程题区域题目表述大致是对于模式串 pabacaba求其next数组next[i]定义为模式串前i个字符组成子串的最长相等前后缀长度。很多人一看到KMP三个字母就直接慌了其实这道题本身并不难它就是单纯考察你对于next数组定义的理解和手算能力。但是这里就出现了一个非常经典的坑——next数组的定义在不同教材、不同资料里并不一致。在我读过的算法书里至少有两种主流定义一种是严蔚敏《数据结构》里的经典定义next数组下标从1开始next[1]0next[i]表示前i-1个字符中最长相等前后缀长度加1。另一种是算法竞赛里常见的prefix function定义next[i]表示模式串前i个字符组成的子串的最长相等前后缀长度下标从0开始。你如果习惯了竞赛的写法用0-based下标去套经典教材的答案或者反过来写出来的数组就会整体差一个量级。我看到当时考场上有几个人在草稿纸上演算了半天最后提交的答案和预期完全对不上大概率就是栽在这个定义差异上。2.2 手算推演一步步推导 pabacaba 的next数组我们来实际推一遍采用竞赛常用的prefix function定义也就是next[i]表示前i个字符组成的子串中最长的相等真前后缀长度。模式串abacaba总共7个字符下标从0到6。next[0]只考虑第一个字符a没有真前后缀结果为0。next[1]考虑ab前缀有a后缀有b不相等结果是0。next[2]考虑aba前缀a等于后缀a长度1前缀ab不等于后缀ba。所以next[2]1。next[3]考虑abac前缀a和c不匹配前缀ab和ac不匹配前缀aba和后缀bac不匹配。结果是0。next[4]考虑abaca前缀a等于后缀a长度1再长的前缀ab等于后缀ca吗不等。所以next[4]1。next[5]考虑abacab前缀a等于后缀b不等前缀ab等于后缀ab相等长度2前缀aba等于后缀cab不等。所以next[5]2。next[6]考虑完整的abacaba前缀a等于后缀a前缀ab等于后缀ba不等前缀aba等于后缀aba相等长度3前缀abac等于后缀caba不等。所以next[6]3。所以最终的next数组就是 [0, 0, 1, 0, 1, 2, 3]。这里有个小技巧可以分享手算next数组的时候从最长的可能长度开始往前试而不是从短到长试。比如算next[6]的时候我先看看长度为6的前后缀是否相等abacab vs bacaba显然不等再看长度为5abaca vs acaba不等然后是长度为4abac vs caba不等接着长度3aba vs aba相等这样一次就能锁定答案不用反复比对。如果从长度1开始试反而容易乱。2.3 这道题真正的考察意图边界情况和基本功出题人把next数组单独拎出来作为一道编程题其实背后有深层用意。KMP算法本身完整写出来大概三十行代码但如果你真的理解了next数组的递推逻辑写出来的代码往往比死记硬背更不容易出错。很多经典实现里next数组递推的核心就是j next[j]这行代码它其实是在利用前面已经算出的next值像滚动一样往前回溯。我当时在草稿纸上重新推导这个递推过程的时候脑子里冒出一个特别直观的类比求next[i]的过程很像是一个人在迷宫里找路如果当前路走不通就回退到上一个分岔口再试。代码里的j next[j-1]就是回退到上一个分岔口的操作。你要是能把这个逻辑内化成直觉遇到再长的模式串也能很快手算出next数组根本不需要死记模板。另外这道题还有个隐含考察点字符串输入的长度和下标越界处理。我见过有人刷题平台提交代码逻辑全对但就是因为遍历时没有处理最后一个字符的边界导致数组越界或者死循环。平时练题喜欢用Python的同学尤其要注意Python的负数下标会让你在边界处理错误时看起来还能运行但结果是错的这种Bug最坑人。3. 编程题拆解两道动态规划与贪心的实战解法3.1 直播间收益最大化问题加权区间调度的动态规划变形E卷四道编程题里有一道我记得特别清楚题目大致是给定若干个直播场次的开始时间、结束时间和预期收益现在需要安排一个直播计划要求任意两场直播不能有时间重叠问怎么安排能使得总收益最大。这个题本质上是经典的加权区间调度问题也是动态规划里非常典型的应用场景。之所以说变形是因为它相比教科书上的标准版多了一个场景包装但核心模型没有变化。拿到这种题第一步不是急着写代码而是先把模型抽象出来每个直播场次是一个带权区间目标是选择一组互不重叠的区间使得权值和最大。标准解法是这样的先把所有场次按结束时间排序。然后定义dp[i]为前i个场次中能获得的最大收益。对于第i个场次只有两种情况——不选它那么dp[i] dp[i-1]选它那么需要找到最后一个结束时间小于等于第i个场次开始时间的场次下标jdp[i] dp[j] 第i个场次的收益。两种情况取最大值。这里最核心的技巧是如何快速找到下标j。如果不做优化每算一个dp[i]都要往前遍历一次整体复杂度是O(n^2)。在笔试环境里如果n达到10^5甚至更大O(n^2)是必挂的。所以必须配合二分查找因为场次已经按结束时间排序了可以用lower_bound或upper_bound找到第一个开始时间大于等于第i个场次开始时间的位置下标减一就是j。我在笔试的时候手写了一版核心逻辑大致是struct Show { int start, end, profit; }; // shows 按 end 升序排序 vectorint dp(n 1, 0); for (int i 1; i n; i) { // 找到最后一个 end shows[i-1].start 的场次 int lo 0, hi i - 1; int j 0; while (lo hi) { int mid (lo hi) / 2; if (shows[mid].end shows[i-1].start) { j mid 1; // dp下标从1开始所以加1 lo mid 1; } else { hi mid - 1; } } dp[i] max(dp[i-1], dp[j] shows[i-1].profit); }这个二分查找是整道题最关键的细节。很多同学知道权值区间调度用DP二分但真正手写二分的时候会在边界条件上栽跟头——到底是用lower_bound还是upper_bounddp下标从0开始还是从1开始中点的处理和答案更新之间的关系是什么这些细节一旦疏忽一个小样例能过大样例就超时或结果错误。我当时用了一个更稳妥的办法把所有场次的结束时间单独存到一个数组ends里然后用upper_bound找到第一个大于当前开始时间的位置再减一。这样就不需要在二分循环里手动维护下标代码更不容易出错逻辑也更清晰。3.2 最少审核员问题贪心堆或者差分数组另一道编程题也很有意思直播平台需要对所有开播场次进行内容审核每个审核员同一时间只能审核一个直播间已知每个直播间的开播时间和结束时间问最少需要多少个审核员才能保证所有直播间都能被覆盖。这就是LeetCode上的会议室II问题属于非常经典的贪心堆的应用。这个题的贪心策略是先按开始时间对所有直播场次排序然后用一个小顶堆维护当前正在进行审核的直播间的结束时间。每当新来一个直播场次先看看堆顶的结束时间是否小于等于当前场次的开始时间——如果是说明已经有一个审核员空闲了那就让该审核员接着服务这个新场次即弹出堆顶把新场次的结束时间压入堆如果不是说明所有审核员都还在忙那就需要新增一个审核员也就是直接把新场次的结束时间压入堆。最后堆的大小就是所需的最少审核员数量。我第一次做这个题的时候曾经陷入过一个思维误区以为要用贪心去选择结束时间最早的审核员实际上小顶堆天然就是每次弹出一个最小的结束时间这个思路完全一致。问题在于很多人在堆操作的时候会忘记处理先弹出后压入的顺序——如果先压入再弹出堆的大小就会多算一个结果就错了。其实这道题还有一个不需要堆的替代解法差分数组。把所有开始时间当成1的事件所有结束时间当成-1的事件然后按时间顺序扫描维护当前同时在播的直播场次数量这个数量的最大值就是答案。这个方法容易理解但是要注意同一时间点的开始和结束事件的先后顺序处理如果处理不好边界在结束事件和开始事件同时发生时容易算错。笔试里推荐用堆的解法因为更不容易出边界问题。3.3 为什么春招算法题偏爱区间调度这类模型你如果仔细研究会发现这两个编程题本质上都是区间相关的模型。为什么出题人喜欢用区间因为区间在真实业务里太常见了直播间时长、任务调度、会议安排、视频切片、资源分配……可以说只要涉及时间线的场景底层都是区间问题。而且区间类问题可以自然地从两个维度演化出不同的考点一是按开始时间处理二是按结束时间处理。按开始时间处理一般配合堆按结束时间处理一般配合DP加二分。你把这两个维度吃透了基本上区间调度、区间合并、最多重叠区间数、最小覆盖区间数这些变体都能顺手解决。春招笔试不是要你背题解而是考察你面对一个看起来陌生的业务场景时能不能快速把它抽象成已知的数据结构和算法模型。所以准备春招的时候我特别建议大家练习题干翻译能力拿到一个题先在纸上写下本质是XX问题再往下想解法。你能在一分钟内完成这个翻译笔试就已经赢了一半。4. 简答题里的机器学习与启发式优化粒子群、ELBO与经典数据结构4.1 粒子群算法的原理与迭代流程从鸟群觅食到公式落地E卷的简答题里有一道简述粒子群算法的原理和基本流程这个题表面上是送分题其实有不少人只写得出粒子群算法是模拟鸟群觅食这一句话后面的公式完全写不完整。在面试官眼里这只能代表你听过这个概念不代表你理解它。粒子群算法的核心是维护一群候选解也就是粒子群。每个粒子有两个属性位置和速度。位置代表当前搜索到的解速度代表下一步移动的方向和步长。每一轮迭代中每个粒子会根据两个最优位置来更新自己的速度一个是个体历史最优pbest另一个是全局最优gbest。速度更新的标准公式是v[i] w * v[i] c1 * r1 * (pbest[i] - x[i]) c2 * r2 * (gbest - x[i]) x[i] x[i] v[i]其中w是惯性权重c1和c2是学习因子r1和r2是[0,1]区间的随机数。这个公式要理解本质第一项wv[i]是惯性表示粒子保持上一轮运动趋势的能力第二项c1r1*(pbest[i]-x[i])是个体认知表示粒子向自己历史上最好位置靠拢的倾向第三项c2r2(gbest-x[i])是社会认知表示粒子向全局最好位置靠拢的倾向。三个力量的平衡决定了粒子群的收敛速度和探索能力。我在笔试时写这个题目除了列出公式还会补上几步流程初始化粒子群的位置和速度计算每个粒子的适应度更新pbest和gbest按公式更新速度和位置重复直到达到最大迭代次数或适应度收敛。这样答就非常完整了。这道题真正想区分的是你有没有真的用代码实现过粒子群如果用过你会知道惯性权重w一般从0.9线性递减到0.4前期希望粒子有较强探索能力后期希望粒子收敛到局部精细搜索如果没用过你只能复述书本上的概念答不出这些实践细节。所以我一直建议准备算法岗笔试的同学简答题相关的经典算法最好亲手实现一遍哪怕跑一个十行代码的demo都比纯背诵有说服力。4.2 KL散度与ELBO变分推断里的关键推导另一道简答题是关于KL散度和ELBO的。说实话这道题出现在春招笔试里对于非机器学习方向的同学有点劝退但对算法岗来说确实是基础中的基础。它考察的是你对变分推断这个框架的理解深度。ELBO证据下界的推导起点是我们想近似后验分布p(z|x)但直接计算它很困难所以找一个简单分布q(z)去逼近它。推导的关键在于把对数边际似然分解成两部分log p(x) ELBO(q) KL(q(z) || p(z|x))其中KL散度恒大于等于0所以ELBO是log p(x)的下界。而这个等式展开会更清楚地看到ELBO的结构ELBO(q) E_{q(z)}[log p(x,z)] - E_{q(z)}[log q(z)]等价地写成ELBO(q) E_{q(z)}[log p(x|z)] - KL(q(z) || p(z))第二种写法更直观第一项是重构似然的期望鼓励q(z)能解释观测数据第二项是一个正则项让q(z)不要偏离先验p(z)太远。这两项的平衡就是变分自编码器VAE里我们熟悉的重构损失和KL正则项。我在答题时会特意指出KL散度的非负性由Jensen不等式保证它是ELBO为什么是下界的唯一关键点。这部分不需要长篇大论把等式写清楚、把每个符号讲解明白阅卷人一看就知道你是真的懂而不是背稿子。4.3 排序算法对比、二分图匹配和HK算法基础知识的覆盖面除了机器学习相关的简答题卷子里还出现了排序算法稳定性、二分图最大匹配等经典问题。这些题在刷LeetCode的同学眼里可能不太起眼但恰恰是决定你是否能进入下一轮面试的分水岭。排序稳定性这道题我答题时选择用表格做对比这样最清晰。快速排序不稳定、堆排序不稳定、选择排序不稳定而归并排序稳定、插入排序稳定、冒泡排序稳定。笔试的时候我建议把时间复杂度、空间复杂度、是否稳定、适用场景四栏都列出来这样既展示了知识面也让答案结构清晰排序算法平均时间复杂度最坏时间复杂度空间复杂度稳定性冒泡排序O(n^2)O(n^2)O(1)稳定插入排序O(n^2)O(n^2)O(1)稳定选择排序O(n^2)O(n^2)O(1)不稳定快速排序O(n log n)O(n^2)O(log n)不稳定堆排序O(n log n)O(n log n)O(1)不稳定归并排序O(n log n)O(n log n)O(n)稳定二分图匹配这道题让我印象比较深的是它延伸到了HK算法Hopcroft-Karp算法。如果你只学过匈牙利算法知道二分图最大匹配用增广路径去找那答匈牙利算法应该能拿一半以上的分。但HK算法是匈牙利算法的优化版本它通过BFS同时寻找多条最短增广路径再用DFS一次性增广把复杂度从O(VE)优化到O(E sqrt(V))。我建议答这类题的时候把为什么需要HK算法说清楚——当图规模很大时匈牙利算法逐条找增广路径太慢HK算法用分层图的思想一次批处理这就是批量增广的思想。4.4 工程场景题为什么笔试里会出现业务场景问题还有一道简答题让我印象很深不是纯算法而是结合了业务场景直播间会出现瞬时高并发评论设计一个实时TOP K热词统计方案。这题已经超出了数据结构本身是典型的算法系统设计混合题。我当时的答题思路是分块处理评论数据先经过消息队列削峰然后由统计服务做滑动窗口内的词频统计对于每个窗口内的词频表可以用哈希表维护但为了取TOP K需要配合一个小顶堆或者跳表。更进一步如果数据量很大还可以用Count-Min Sketch这类概率性数据结构做近似计数空间上更省但会有一定的误差。这种题的考察点很明确你能不能把课堂上学到的数据结构和算法迁移到真实的业务场景里去解决问题。单纯背堆排序、背哈希表遇到这种需要权衡取舍的问题就露馅了。备考阶段可以多问问自己我学的这个数据结构在真实系统里可以用来解决什么痛点多这样想几次笔试答题会流畅很多。5. 复盘与避坑这些细节决定你是AC还是挂零5.1 KMP的next数组两种下标定义的混淆是最大的坑我已经在第二章详细算过next数组但这里还是想再强调一遍这个坑因为它是E卷编程题第一题里最经典、也最不值得一提的送命题你计算出来的数组到底用的是哪种定义。刷题平台的判题系统通常会把输入输出格式写得很清楚但平台给出的示例可能只有一两个不足以让你判断定义是否一致。如果你实现的是0-based prefix function但题目期望的是1-based经典定义那你在第一个测试用例上就会错甚至白白浪费二十分钟去debug一个代码完全正确的程序。我的经验是动手写代码前先扫一眼题目里的next[i]定义为这句话如果它写的是前i个字符那就是0-based prefix function如果写的是第i个字符之前的子串匹配长度则更接近经典定义。如果题目描述模糊那就用最朴素的暴力法先计算一遍样例确认定义后再写优化代码。宁可多花两分钟确认也比写完才发现整体偏移好得多。5.2 动态规划题里的二分查找边界lower_bound还是upper_bound在3.1节那道区间调度DP题中二分查找的边界问题值得单独再展开。很多人写完DP逻辑之后信心满满结果一跑测试就超时或者答案错误十有八九是二分查找写错了。我推荐的写法是用upper_bound查找第一个结束时间大于等于当前开始时间的下标然后减一拿到目标位置j。那么为什么不能用lower_bound因为我们要找的是最后一个结束时间小于等于当前开始时间的场次upper_bound返回的是第一个大于当前开始时间的迭代器减一之后正好是小于等于的最后一个而lower_bound返回的是第一个大于等于当前开始时间的迭代器减一之后是严格小于的最后一个会把结束时间恰好等于当前开始时间这种情况漏掉导致结果比最优解偏小。这个区别其实非常容易忽略因为笔试环境里你可能只测一两个简单用例两者的结果甚至可能是一样的。但一旦构造一个前一场直播的结束时间正好等于下一场开始时间的测试用例lower_bound的写法就会出错。建议大家在平时刷题的时候就把这种边界情况当成默认测试条件想清楚每个函数返回的含义再动手写。5.3 堆的用法先弹出再压入顺序别搞反回到3.2节的最小审核员问题堆操作里有个特别容易出错的顺序问题。按照正确逻辑处理一个新的直播场次时应该先判断堆顶的结束时间是否早于等于当前开始时间如果是则弹出堆顶表示分配一个空闲审核员然后压入当前场次的结束时间表示该审核员接下来负责这个新场次。我见过反过来的写法先把当前场次压入堆再弹出堆顶。如果堆顶恰好是当前压入的这个元素弹出之后堆的大小不减反增最后的答案就会偏大。而且这种错误在简单用例里不容易发现因为压入再弹出有时看起来也能正常工作但一旦堆顶不是当前元素答案就会明显不对。建议写这个题的时候把弹出旧任务和压入新任务当成两个独立的操作分开写不要合在一行里为了省代码而搞出副作用if (!minHeap.empty() minHeap.top() shows[i].start) { minHeap.pop(); // 旧直播结束这个审核员空闲了 } minHeap.push(shows[i].end); // 分配审核员这个顺序写对了逻辑就清晰了也方便后面调试。5.4 机试环境的细节输入输出格式、内存限制与编译器版本最后这部分很琐碎但每年都有不少人栽跟头。E卷是在在线评测系统上做的有三个很现实的坑需要注意。第一个坑是输入输出格式。有些题目会同时存在多组输入有的则是单组输入有的行末有空格也算对有的则严格不允许。笔试开始前一定要先看平台的输入输出说明提前在本地写一个处理模板比如常用的快读快输、EOF判断循环这些。我习惯在笔试前把一段通用的输入模板直接敲好这样可以省下每道题都要重新写输入解析的时间。第二个坑是内存限制和递归深度。很多算法题要求空间复杂度控制在O(n)以内如果你一开始写的是递归版本可能会因为递归深度过大导致栈溢出。特别是图论的DFS、区间DP这类问题笔试时建议改成显式栈或迭代写法。E卷里那道图论变体题如果使用递归DFS当节点数量达到十万级别时Python的默认递归深度根本不够用但平台又不会让你改递归限制所以一开始就用栈模拟最稳妥。第三个坑是编译器版本差异。如果你平时用C有些平台的编译器是C14有些是C17std::gcd这种函数在C14里可能不存在会直接编译报错。所以写代码时尽量使用所有版本都支持的通用语法如果确实要用新特性可以自己实现一个简单的替代函数避免编译不过带来的无谓扣分。笔试结束后我还整理过一份答题卡自查清单每道题是否考虑了空数组、单个元素、全相同元素、极端大数这些边界情况是否有数组越界风险是否用了不该用的全局变量是否忘记释放内存C。每次笔试前过一遍这个清单能避免很多低级失误。哪怕多花五分钟检查边界也比你急匆匆提交一个系统判错要强得多。一点不成熟的小体会做完映客这套2020春招算法E卷之后我最大的感受是这套卷子并没有用稀奇古怪的算法来炫技反而特别看重基本功的稳定性。你把KMP的next数组吃透把区间调度DP吃透把堆的经典应用吃透把粒子群这类常见的启发式算法讲清楚分数就不会低。这和直播业务本身对算法工程师的要求也挺匹配真实业务中的很多问题本质上都是经典模型套了一层业务壳你剥壳的能力决定了你能不能在团队里快速落地。准备春招的朋友与其焦虑地去刷各种冷门偏题不如把经典模型里的为什么想透把边界条件练到肌肉记忆。毕竟笔试考的不只是你会不会写代码更是你在有限时间和压力下能不能交出稳定、可用、能跑的方案。