ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

B站2020校招算法笔试题复盘:KMP、动态规划与避坑指南

B站2020校招算法笔试题复盘:KMP、动态规划与避坑指南 2020年秋天我在牛客上翻到这份《哔哩哔哩2020校园招聘算法笔试卷二》的时候第一反应是这卷子出得挺有水平。后来秋招结束再回头看才发现每道题背后其实都在挑一种能力——不是你能不能背出某个公式而是你在有限时间内能不能把问题抽象成算法模型。这篇复盘我不会贴原题截图而是把当时考完印象最深的几个知识点、编程题的完整思考过程还有那些在判题系统里踩过的坑全部分享出来。适合正在准备大厂算法岗笔试的人参考无论是B站还是其他视频类互联网公司这套打法是通用的。1. 先把这份卷子的考试策略聊透1.1 视频平台算法岗到底在考什么很多人看到“哔哩哔哩算法笔试”第一反应是是不是会考推荐系统、视频理解、弹幕情感分析说实话2020年这道卷子给我的感觉很务实——它没有直接考你“如何设计一个推荐系统”而是把推荐系统背后真正用得上的基础能力拿出来考字符串处理、排序、动态规划、概率统计、机器学习基础。这其实符合国内视频平台的招聘逻辑。算法岗进去之后第一年基本都在做数据清洗、特征工程、模型训练流程的搭建真正上手推荐模型设计是后面的事情。笔试阶段重点看的不是你懂多少最新的Transformer变体而是你写代码的基本功、对经典模型的理解深度以及在两个小时内解决未知问题的思路。B站当时的业务体量已经很大弹幕、评论、稿件这些内容数据都依赖高效的字符处理和检索算法所以字符串相关的题出现频率很高。1.2 题型结构带来的做题节奏变化这份卷子给我的整体节奏感是选择题不急编程题要果断。算法笔试卷通常有两种风格一种是选择题刁钻到让你怀疑人生编程题反而简单另一种是选择题基础但面广编程题是真正的分水岭。《哔哩哔哩2020校园招聘算法笔试卷二》属于后者。我记得当时时间分配大概是选择题控制在40分钟以内剩80分钟全部留给编程题和问答题。比较理想的做法是拿到卷子先把所有题目扫一遍挑出“一眼就知道思路”的题先做掉。遇到卡壳超过5分钟的选择题随手标一个最可能的答案继续往下走。这个策略在真实笔试里比多复习一百道题还管用。1.3 知识点覆盖范围把这些年刷过的几十套互联网大厂笔试卷放在一起对比你会发现B站的考点分布和主流视频平台非常接近。下面这张表是当时我考完根据自己的记忆整理的覆盖情况模块考点出现形式数据结构KMP next数组、堆、二叉搜索树选择题经典算法排序复杂度、贪心、动态规划选择编程图论最短路、拓扑排序选择/简答机器学习KNN、决策树、过拟合、损失函数选择问答概率统计贝叶斯、期望计算选择题编程题字符串滑动窗口、背包类DP在线编程这个覆盖范围说明一件事B站招算法岗并不是只要推荐方向的内容安全审核、弹幕特征提取、用户增长实验、搜索排序这些岗位对基础算法的要求是一致的。所以准备笔试的时候千万别只刷机器学习题数据结构和算法的权重至少在六成以上。2. 精选选择题与基础算法深度拆解2.1 KMP的next数组为什么这么让人头痛热搜词里有一条很典型“在KMP算法中对于模式串pabacaba其next数组(next[i]定义为…”。这句话基本就是原题考法。KMP作为字符串匹配的经典算法在校招笔试里出现频率极高。它考的并不是你能不能背出代码而是你能不能把next数组的物理意义讲清楚。先说定义。这里有个特别容易踩坑的地方next数组有两种常见定义。第一种定义是next[i]表示模式串前i个字符组成的子串中最长相等前后缀的长度第二种定义是next[i]表示在模式串第i个位置失配时模式串指针应该回退到的位置。两种定义算出来的数组数值不一样网上很多题解混着用导致很多人越看越乱。我用第一种定义来算p abacabai1子串a没有真前后缀next[1] 0i2子串ab前缀a、后缀b不相等next[2] 0i3子串aba前缀a等于后缀a长度为1next[3] 1i4子串abac前缀a、后缀c不相等next[4] 0i5子串abaca前缀a等于后缀a长度为1next[5] 1i6子串abacab前缀ab等于后缀ab长度为2next[6] 2i7子串abacaba前缀aba等于后缀aba长度为3next[7] 3如果按第二种定义通常还会令next[0] -1next[i]存的是长度本身这样失配后模式串指针就跳到next[i]的位置继续比较。我当时总结了一个“为什么KMP容易懵”的规律人脑处理“回退”这个概念很吃力因为匹配是顺序的而回退是逆着记忆走的。所以我后来给自己定的死规矩是遇到KMP直接先把前缀函数prefix function画出来用表格一行一行填别心算。笔试选择题允许你在草稿纸上认真推这题只要细心拿分问题不大。2.2 排序算法、堆与TopK的复杂度纠结点B站的笔试卷里排序相关的内容几乎是必考。常见考法就是给你几种排序算法问你最好、平均、最坏情况的时间复杂度以及是否稳定。这个知识点没有太多技巧就是死记硬背加理解原理。我特意做了一张对照表方便记忆排序算法平均时间复杂度最坏时间复杂度是否稳定核心记忆点冒泡排序O(n²)O(n²)稳定相邻交换快速排序O(n log n)O(n²)不稳定分区递归归并排序O(n log n)O(n log n)稳定分治合并堆排序O(n log n)O(n log n)不稳定建堆调整插入排序O(n²)O(n²)稳定数据量小时最优有一个细节我要专门说一下堆排序虽然时间复杂度漂亮但是工程上很少用它做常规排序。因为堆排序的缓存局部性很差数组元素在存储器中跳来跳去实际跑起来往往比快排慢。大家常说的“快排虽然最坏情况退化到O(n²)但通过随机化基准值可以极大降低退化概率”这句话在笔试问答题里就是关键得分点。TopK问题是排序的进阶考法笔试里会以“从一亿个整数中找出最大的K个数”这种形式出现。考优化的思路就是使用大小为K的最小堆遍历数据时如果当前元素大于堆顶就替换堆顶并下沉调整。如果K远小于N可以把时间复杂度从O(N log N)降到O(N log K)。如果K也很大那就改用快速选择算法平均O(N)但存在最坏情况风险。这几个方案我都在实际工程里用过给你一个结论生产环境数据量大时最小堆方案最稳因为你只需要保证堆不溢出不需要关心输入数据是分布均匀还是极端有序。2.3 贪心和动态规划边界怎么切选择题里经常给一个情景让你判断“用贪心还是用DP”。这是一个非常经典的考察逻辑。我总结的判断方法是你问自己一个问题——“这一步做了最贪的选择之后还有没有可能必须回去修改这个选择”如果不用改贪心如果可能改DP。举个例子。找零钱问题硬币面额是[1, 5, 10, 20]目标金额是36要求用最少硬币数量。在这个特定面额组合下贪心算法是可行的。但你只要把硬币面额换成[1, 5, 11]目标金额15贪心就会出错——贪心会先拿11剩下4只能用4个1总数5枚而最优解是3个5总数3枚。因为第一步拿11就“堵死了”后续的最优解必须回头修改决定这种情况就只能用DP。DP的核心是定义状态和转移方程。当时做笔试时我习惯先不用代码写而是在草稿纸上把状态转移方程写好再和题目给的数据范围对照确认时间复杂度是否可接受。这个习惯帮我避开了很多“看懂了思路但代码写崩”的情况。2.4 图论小题Dijkstra和Kahn算法2020年的卷子里图论不是重点但Dijkstra最短路、Kahn拓扑排序这两个名字出现在当时的备考点里。Dijkstra算法用优先队列优化的版本时间复杂度是O((VE) log V)笔试常考它的适用范围——不能处理负权边如果题目里出现负权就要考虑Bellman-Ford或SPFA。Kahn算法是拓扑排序的实现方式之一核心思路是每次从图中取出一个入度为0的节点把它加入结果序列然后删除它的所有出边继续循环。这个算法在现实业务里有个特别有意思的应用——视频平台的任务依赖调度。比如B站要把一个视频转码成多清晰度版本转码任务之间存在依赖关系某些任务必须等前置任务完成才能执行拓扑排序给的就是一个合法的任务执行顺序。如果你在图论方面时间有限优先掌握Dijkstra的堆优化写法以及Kahn拓扑排序的代码这两个是性价比最高的。邻接表存图是基本功笔试时一定要能在5分钟内写完。3. 编程题实战从读题到AC全流程3.1 一道典型的字符串编程题编程题当时有一道很典型的题目背景大概是这样给定一个字符串s找出其中不含重复字符的最长子串的长度。这个题目本质上是LeetCode的Longest Substring Without Repeating Characters但披了一层B站弹幕的皮比如把s称为弹幕内容。看到这种背景别慌核心就是把题目还原成最原始的算法模型。题目翻译过来就是给定一个字符串找最长连续子串使得子串中所有字符互不相同。这里有两个关键词需要抓住“连续”和“不含重复字符”。连续决定了它适合用滑动窗口而不是简单的集合去重不含重复字符决定了我们需要一个哈希表记录窗口内每个字符的位置。3.2 暴力解到最优解的演化过程拿到这个题正常人第一反应是暴力枚举所有子串。检查所有起点i和终点j然后对每个子串判断是否有重复字符时间复杂度O(n³)这个复杂度在字符串长度超过1000时基本跑不动。进化的思路是用滑动窗口。核心逻辑是右指针不断向右扩展把新字符加入窗口发现重复字符时左指针跳到重复字符上次出现位置的右边一位。这里有个关键细节左指针不能用left 1的方式一步一步走而是应该直接跳到指定位置这样才能保证整体复杂度是O(n)。我在考场上犯过一个错左指针移动时忘了取max。因为可能出现这种情况——左指针已经跳到了一个比较靠右的位置而新发现的重复字符的上次出现位置在左指针左边如果直接跳过去会导致左指针倒退破坏窗口的单调性。正确写法是用left max(left, last_pos[char] 1)。3.3 完整代码与边界细节这道题的C实现长这样#include iostream #include string #include unordered_map using namespace std; int lengthOfLongestSubstring(string s) { unordered_mapchar, int lastPos; int left 0; int ans 0; for (int right 0; right s.length(); right) { char c s[right]; if (lastPos.find(c) ! lastPos.end()) { left max(left, lastPos[c] 1); } lastPos[c] right; ans max(ans, right - left 1); } return ans; }边界情况要单独验证几类。字符串为空时循环不执行ans返回0正确。字符串全是相同字符时比如aaaaleft每次都跳到上一次位置的后一位ans始终为1正确。字符串长度为1时ans为1正确。字符串中所有字符都不重复时left永远不会移动right从0走到末尾ans就是字符串长度正确。考场上写完代码一定要手动跑一遍这四种边界输入花不了两分钟但能避免大量提交后才发现漏写边界情况的尴尬。3.4 再补充一道背包类动态规划题编程题除了字符串动态规划也是重头。当时考过一个类似0-1背包的题目有n个题目第i个题目完成需要time[i]时间能获得score[i]的分数总的考试时间是T问最多能获得多少分。这个题把背景去掉就是经典01背包。状态定义是dp[i][j]表示前i个题目在总时间j内能获得的最大分数转移方程是不完成第i个题目dp[i][j] dp[i-1][j]完成第i个题目前提是j time[i]dp[i][j] max(dp[i][j], dp[i-1][j-time[i]] score[i])空间优化可以用一维数组倒序更新#include iostream #include vector #include algorithm using namespace std; int maxScore(int T, vectorint time, vectorint score) { vectorint dp(T 1, 0); for (int i 0; i time.size(); i) { for (int j T; j time[i]; j--) { dp[j] max(dp[j], dp[j - time[i]] score[i]); } } return dp[T]; }注意那个倒序循环。正序循环会导致同一个题目被重复拿相当于把01背包变成了完全背包。这是这道题最经典的坑考场上我见过好几个同学在这里翻车。我当时还在草稿纸上推导了一遍为什么倒序能避免重复——因为一维数组更新时dp[j - time[i]]是上一轮的状态如果正序dp[j - time[i]]可能已经被本轮更新过了污染了转移来源。4. 机器学习、深度学习和概率统计考点复盘4.1 机器学习的“送分题”也有陷阱B站作为内容平台用户画像和推荐系统依赖于机器学习模型所以笔试必然考机器学习基础。KNN、决策树、朴素贝叶斯、逻辑回归这些都是基础中的基础看起来是“送分题”但实际上有陷阱。以KNN为例常见考法是问K值大小对模型的影响。K值过小模型容易受到单个噪声样本的干扰发生过拟合K值过大分类边界过于平滑把局部结构也忽略掉了发生欠拟合。另一个考点是KNN的“距离度量”——欧氏距离适合连续数值特征但如果特征之间量纲差异很大必须先做标准化否则量纲大的特征会主导距离计算。比如视频的播放量是百万级、点赞量是万级不标准化直接算距离播放量就会完全压过点赞量。决策树考点主要是信息增益和基尼指数的计算以及连续特征的切分点选择。连续特征的处理是排序后找相邻值的中点一个个试。这个知识点在笔试里可能会出一道计算题给你一个表格让你算按某个阈值切分后基尼指数下降多少。这个计算量不大但需要你熟练记忆公式考场上现推很容易出错。4.2 损失函数、优化过程与过拟合深度学习部分我当时遇到的考点集中在损失函数和过拟合手段。交叉熵几乎是所有分类模型的标准损失函数它的形式是L -∑ y_i * log(p_i)笔试会问它和KL散度的关系。这里有一个理解深度的分水岭交叉熵 真实分布的熵 KL散度。当真实分布是独热编码时真实分布的熵为0最小化交叉熵就等价于最小化KL散度。能答到这一层说明不是死记公式。过拟合问题基本是必考。防止过拟合的办法有正则化L1、L2、Dropout、数据增强、早停等。L1正则化和L2正则化的区别经典考法是你知不知道L1能产生稀疏解、L2只能让权重变小但不为0。背后的原因是L1的不可导点在坐标轴上优化过程容易把参数推到0。笔试如果是简答题最好再补一句“L1正则化相当于给参数加了一个拉普拉斯先验L2正则化相当于加了高斯先验”这句话能显著提升答题深度。4.3 概率统计小题计算概率统计在算法笔试里占的比例不高但一旦出现就不会是纯粹的送分题。我当时遇到的是贝叶斯公式的套用。题目大意是某种内容被审核系统标记为违规的概率召回率和实际违规的概率问一条被标记的内容真实违规的概率是多少。这种题就是标准的贝叶斯公式计算题。设违规为先验概率标记为敏感内容为观测事件套公式就能算。很多人做错是因为没有正确区分P(违规|标记)和P(标记|违规)。在准备这个考点时我建议多花点时间在条件概率、全概率公式、独立性判断这几个知识点上不需要刷太偏的概率难题。5. 笔试实战中的避坑清单与复盘方法5.1 做题顺序和时间分配怎么安排最稳笔试过程中最怕的不是题难而是时间用完但后面有简单题没做。我当时的策略是“三遍法”第一遍快速扫描所有题目把一眼能看出答案的搞定第二遍静下心做需要思考的选择题和简答题第三遍集中精力写编程题。编程题的做题顺序也有讲究。先做数据范围最小的那道因为通常复杂度要求低代码更容易写对再去做字符串、模拟类题目最后做动态规划类。这个顺序的逻辑是越难的题越需要冷静的脑力放在后面反而因为时间压力做不出来倒不如先拿稳能拿的分数。5.2 判题系统和输入输出容易踩的坑在线编程和本地IDE有本质区别。本地IDE你输入多少次都行判题系统是黑盒只会告诉你“通过率0%”或“超时”。有几类高频坑第一多组输入问题有些题目需要循环读入直到EOF你写一次读入就会只过一个用例第二数值溢出题目只说了整数没说范围尽量用long long第三输出格式多余的换行或空格会让字符串比较失败。这个“输出不能有多余空格”的坑我在正式笔试里踩过。当时判断输出的最后一个字符时我写了个特判结果特判条件写反了导致最后一组数据后面多了一个空格整道题判0分。从那以后我给自己定了一个死规矩输出使用统一的辅助函数或辅助string把所有输出内容先拼接好最后一次cout彻底避免空格问题。5.3 一套高效的复盘方法笔试结束后不管通过与否花时间复盘比投下一家更重要。我的复盘方法是把错题分成三类第一类是计算失误或粗心这类不用再刷相关专题只需要提醒自己下次检查时间第二类是“知识盲区”就是完全没见过这个知识点需要立刻补齐原理并刷至少5道同类型题第三类是“想得出思路但代码写不对”这类最需要警惕说明基本功不够需要刷大量的类似题把代码能力提上去。我当时准备了一份错题表用表格记录题目名称、考点、错误原因、正确思路。每周回顾一次看似费时间但比盲刷一整天题库效率高得多。面对校招这种长线作战稳住节奏比爆发式努力更关键。写在最后的一个小经验如果你现在正准备算法笔试我想分享一个很多人忽略的点笔试考的是“在压力下做决策”的能力。你不可能每道题都用最优解写出来有时候一道题用O(n²)的解法也能通过测试用例因为判题机的数据范围限制了最坏情况。所以遇到没思路的题先写暴力解拿部分分再优化这比死磕最优解高效得多。2020年那场B站笔试我其实有两道选择题都没完全确定答案但编程题拿稳了最后一样进面试了。稳扎稳打把会做的全做对结果一般不会差。
返回列表