爱奇艺算法工程师笔试复盘:KMP、动态规划与拓扑排序考点解析
2026/8/29 5:33:34 网站建设 项目流程

爱奇艺2018秋季校招算法工程师那场笔试,我印象挺深。当时我在学校边刷题边投简历,对视频平台的算法岗特别好奇——毕竟搜索、推荐、内容理解这些方向,听起来就比纯业务后台有意思。第二场笔试题做下来,最大的感受是:它不像有些公司那样只堆机器学习理论,而是把数据结构与算法的基础考得很扎实,同时穿插概率统计和深度学习的基础题,整体风格务实,贴近实际业务场景。

这篇文章我按当时的考场回忆和后来复盘的思路整理出来,适合正在准备互联网公司算法岗校招的同学,尤其是目标在视频、内容平台方向的人。哪怕你暂时不投爱奇艺,这套题型结构和考点逻辑也很有参考价值——算法岗笔试讲到底就是数据结构、机器学习、概率统计和编程题四块,换哪家公司都一样。

1. 笔试整体设计与考点分布

1.1 题型结构与考察目标

2018年秋季校招的第二场笔试,形式上和其他大厂差别不大,主观题和客观题混合,整体题量中等偏大。我当时印象里大约是20道左右的选择题加上4道编程题,总分里编程题占大头,客观题用来卡基础。

选择题又分单选和多选。单选考的是确定性的知识点,比如KMP算法中next数组的计算、堆排序的时间复杂度、某个概率分布的期望——这些有标准答案,不会就是一锤子买卖。多选则是拉分项,经常出现“以下哪些方法可以缓解过拟合”这种题,选多选少都扣分,得对知识点有完整理解才能拿稳。

编程题是整场笔试真正的分水岭。算法岗的编程题和开发岗有个明显区别:开发岗偏重工程实现和API熟练度,算法岗则更偏重“建模能力”——把问题翻译成数据结构与算法的能力。爱奇艺的编程题基本覆盖了字符串处理、动态规划、贪心、图论四大类,全是LeetCode中等偏上的难度,不会给特别偏的题目,但每一道都有陷阱。

1.2 考点分布与业务方向的关系

我后来复盘时把考点整理了一张表,对照爱奇艺的业务场景看特别有意思:

考察模块具体考点对应业务场景
字符串与模式匹配KMP、正则、编辑距离弹幕关键词过滤、内容审核
动态规划序列DP、背包问题、编辑距离推荐排序、路径规划
贪心与堆任务调度、区间覆盖转码任务调度、CDN资源分配
图论拓扑排序、最短路、二分图用户关系链、视频知识图谱
机器学习正则化、梯度下降、过拟合CTR预估、推荐排序
深度学习CNN/RNN基础、激活函数视频分类、封面识别、语音字幕
概率统计贝叶斯、分布、期望A/B实验、置信度评估

这份表格是我考完以后才想明白的。爱奇艺作为视频平台,算法工程师的工作不只是做推荐,还涉及视频内容的理解、审核、转码调度、弹幕分析等,所以考察面会比纯电商平台更广一些。比如图论那道题,表面上是考拓扑排序,实际上对应的是视频之间的依赖关系——一个剧集的季与集之间、一条转码任务的前后依赖,都是典型的DAG问题。

1.3 时间分配才是隐藏的第一道题

笔试时间一般90到120分钟,我那次大概是100分钟上下。很多同学栽在时间分配上:选择题磨太久,编程题反而没时间写完整。

按我的经验,合理的分配应该是这样:

  • 客观题控制在30到35分钟,单题不超过2分钟,没把握的先标记跳过
  • 编程题每道预留15到20分钟,先整体扫一遍四道题,从最简单的入手
  • 最后留5分钟检查代码边界条件

这个节奏我一直用到现在面试别人做白板题时也在推荐:笔试考的不是“你能不能做出最难的题”,而是“你在有限时间内能不能拿到最多的分”。

2. 客观题核心考点解析

2.1 KMP算法:next数组的计算套路

选择题里出现了一道很经典的题——给定模式串p="abacaba",求它的next数组。这题我印象太深了,因为当年刚刷KMP时被next数组的各种定义搞晕过,复习了整整两天才理顺。

首先要明确next数组到底怎么定义。国内教材常见两种定义:

  • 一种是最长相等前后缀长度,next[i]表示p[0..i]中前缀等于后缀的最大长度
  • 另一种是失配时的跳转位置,往往取最长相等前后缀长度减一,且next[0]=-1

这两种在代码实现里都有,关键看题目用哪种。按考场上最常见的前缀函数定义,next[i]是p[0..i]的最长相等前后缀长度,手算过程如下:

字符串: a b a c a b a 下标: 0 1 2 3 4 5 6 next: 0 0 1 0 1 2 3

推导逻辑:next[0]=0,单个字符没有真前后缀;到"ab",前缀a不等于后缀b,next[1]=0;到"aba",前缀a等于后缀a,next[2]=1;到"abac",四个前缀和四个后缀两两对比,没有相等项,next[3]=0;再到"abaca",前缀a等于后缀a,next[4]=1;到"abacab",前缀ab等于后缀ab,next[5]=2;到"abacaba",前缀aba等于后缀aba,next[6]=3。

这道题真正想考的并不是你会不会算,而是你有没有理解KMP在匹配失败时如何利用next数组跳过不必要的比较。很多同学把next背下来了,但换个形式考就懵了。比如考“在匹配到某个位置失配后,模式串该右移几位”,其实就是利用next数组的性质推导。

2.2 数据结构基础:排序与堆的边界条件

多选题里有一道关于排序算法稳定性的题,选项涉及快排、归并、堆排序、冒泡排序。这里有个常见误区:快排是不稳定排序,虽然它平均复杂度O(nlogn)很快,但相等的元素可能在分区时交换相对顺序;归并排序是稳定排序;堆排序不稳定,因为建堆和堆调整过程中会打乱相同元素的相对位置;冒泡排序是稳定排序。

除此之外还考了堆排序的建堆时间复杂度。正确选项是O(n),不是O(nlogn)。很多同学误以为建堆要逐个插入,其实从最后一个非叶子节点开始自底向上做sift-down,整体复杂度是O(n),这个结论《算法导论》里有严格的累和证明,面试时也经常被问到。

2.3 机器学习基础:正则化与过拟合

客观题里机器学习部分占了不少比例,基本是基础题,但基础题也有区分度。比如一道多选:“以下哪些方法可以缓解过拟合?”选项包括L1/L2正则化、增加训练数据、降低模型复杂度、增加模型深度。

前三个是对的。增加深度通常会让模型容量变大,反而更容易过拟合——除非配合正则化和更多数据。这里我想多说一句:L1和L2正则化的区别也是高频考点,L1会把权重推向0,产生稀疏解,适合特征选择;L2只会让权重变小但不会归零,适合防止权重过大。理解这个区别,比死记“L1稀疏L2平滑”更有用,因为可以推导出来:L1的梯度是常数,在0附近有不可导点,迭代时更容易落在0上。

还考了梯度下降的变体,比如SGD、Momentum、Adam的区别。其中Adam结合了动量和自适应学习率,是目前深度学习训练中的主流优化器,这个知识点在笔试里出现频率极高。

2.4 概率统计与深度学习基础

概率题有一道让我印象很深:给一个事件发生概率p,问重复n次至少发生一次的期望和概率。这类题在业务里对应的是A/B实验的置信度判断——你做了多组实验,至少一组显著是“假阳性”的概率有多大,用1-(1-p)^n来算。其实在推荐系统评估里,这个思想经常用到。

深度学习部分考了激活函数的选择和梯度消失问题。问“在深层网络中,以下哪个激活函数最能缓解梯度消失”,答案基本锁定ReLU。原因是sigmoid/tanh在输入绝对值大时梯度趋近于0,反向传播时连乘会让梯度指数级缩小;ReLU在正区间梯度恒为1,可以缓解梯度消失,但要注意ReLU在负区间梯度为0,可能导致神经元“死亡”,所以后来才有LeakyReLU等变体。

3. 编程题实战复盘与代码实现

编程题4道,我在考场上只完美AC了两道,一道做了部分case优化勉强通过,一道只有暴力解。复盘的时候重写了一遍,这四道题其实都挺经典。

3.1 字符串:模式匹配与next数组计算

第一道题是直接让实现KMP算法,匹配一个字符串在另一个字符串中首次出现的位置。题面看起来直白,但要求写出完整的next数组计算过程,并且用KMP完成匹配。这题在LeetCode上是原题(实现strStr),难度中等。

KMP的核心思想是:主串指针不回退,利用next数组决定模式串跳到哪个位置继续匹配。主串i一直往前走,模式串j根据next数组回退,这样主串每个字符最多被比较一次,整体O(m+n)。

def get_next(p): n = len(p) next = [0] * n j = 0 for i in range(1, n): while j > 0 and p[i] != p[j]: j = next[j-1] if p[i] == p[j]: j += 1 next[i] = j return next def kmp_search(s, p): if not p: return 0 next = get_next(p) j = 0 for i in range(len(s)): while j > 0 and s[i] != p[j]: j = next[j-1] if s[i] == p[j]: j += 1 if j == len(p): return i - j + 1 return -1

考场上写KMP最容易出bug的点有两个。一个是next数组的索引和边界,另一个是匹配失败时回退的写法。建议先在纸上把next数组手算一遍,再对照代码检查,能大幅降低出错率。

3.2 动态规划:编辑距离

第二道编程题是编辑距离(Edit Distance),给出两个字符串word1和word2,允许插入、删除、替换三种操作,求把word1变成word2的最少操作次数。这是DP的经典题,LeetCode 72题原题,考察的是状态定义和转移方程。

状态定义是dp[i][j]表示word1前i个字符变成word2前j个字符的最少操作数。初始化:dp[i][0]=i,表示把word1前i个字符全部删除;dp[0][j]=j,表示把空串变成word2前j个字符,需要全部插入。

转移方程分两种情况:

  • 如果word1[i-1]==word2[j-1],则dp[i][j]=dp[i-1][j-1],不需要额外操作
  • 否则dp[i][j]=min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])+1,分别对应删除、插入、替换
def min_distance(word1, word2): m, n = len(word1), len(word2) dp = [[0] * (n+1) for _ in range(m+1)] for i in range(m+1): dp[i][0] = i for j in range(n+1): dp[0][j] = j for i in range(1, m+1): for j in range(1, n+1): if word1[i-1] == word2[j-1]: dp[i][j] = dp[i-1][j-1] else: dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 return dp[m][n]

这题考场上我也踩了坑:初始化的两重循环写到后面,dp[0][j]被默认0覆盖了,结果跑出来全是错。后来检查了10分钟才发现。笔试里这种低级失误特别冤枉,建议初始化部分单独用注释标出来。

面试时如果考到编辑距离,大概率还会追问“如何优化空间复杂度”。优化思路是用滚动数组,因为dp[i][j]只依赖dp[i-1][j]、dp[i][j-1]、dp[i-1][j-1]三个状态,所以可以用两个一维数组滚动更新,空间从O(m*n)降到O(n)。如果想让难度再往上走,还会问“如何输出具体的编辑路径”,那就是在DP回溯加上方向记录,属于进阶玩法。

3.3 贪心加堆:任务调度器

第三题是一道任务调度问题,题面我简化一下:给定一个任务列表,每个任务用大写字母表示,相同任务之间必须间隔至少n个单位时间,求完成所有任务的最短时间。这是LeetCode 621题“任务调度器”的变体,核心思路是贪心加计数。

思路是:出现次数最多的任务决定了最短时间的下限。比如任务A出现k次,间隔是n,那么至少需要(k-1)*(n+1)+1个单位时间。但如果有多个任务出现次数同样最多,比如A和B都出现k次,那最后一个区间里要同时放下A和B,总时间就要加1。

def least_interval(tasks, n): freq = [0] * 26 for t in tasks: freq[ord(t) - ord('A')] += 1 freq.sort(reverse=True) max_count = freq[0] idle_slots = (max_count - 1) * n for i in range(1, 26): idle_slots -= min(freq[i], max_count - 1) idle_slots = max(0, idle_slots) return len(tasks) + idle_slots

这题在当时算中等偏上的难度,考场上容易纠结于模拟整个调度过程,其实贪心解法非常简洁。关键是要理解:不需要真的按时间轴模拟,只要算出有多少空闲槽位被其他任务填满就行。刷过的人5分钟能AC,没刷过的可能30分钟还卡在怎么模拟。

3.4 图论:课程表拓扑排序

第四道编程题是经典的课程表问题:一共有numCourses门课程,给定一些先修关系对,比如[1,0]表示学课程1之前必须先学课程0,问是否可能完成所有课程。本质上就是判断有向图是否存在环,有环就完不成,典型解法是拓扑排序。

我用的思路是Kahn算法,维护每个节点的入度,先把所有入度为0的节点入队,依次出队并更新相邻节点的入度,最后如果出队的节点数不等于总节点数,说明有环。

from collections import deque def can_finish(numCourses, prerequisites): graph = [[] for _ in range(numCourses)] indegree = [0] * numCourses for course, pre in prerequisites: graph[pre].append(course) indegree[course] += 1 q = deque([i for i in range(numCourses) if indegree[i] == 0]) count = 0 while q: node = q.popleft() count += 1 for nxt in graph[node]: indegree[nxt] -= 1 if indegree[nxt] == 0: q.append(nxt) return count == numCourses

这题还有一种DFS判环做法,用状态数组标记每个节点的访问状态(0未访问、1访问中、2已完成),如果在DFS过程中碰到状态为1的节点就说明发现环。两种方法我都建议掌握,Kahn适合求具体拓扑序列,DFS判环代码更短。面试时如果追问“如何输出一种合理的修课顺序”,用Kahn就能直接输出,所以Kahn在实际中更常用。

4. 考场时间分配与避坑指南

4.1 时间分配策略

前面提到过,客观题控制在30到35分钟。但这里有个细节:多选和单选的策略完全不一样。单选题如果完全不会,可以靠排除法提高命中率,但不要空着,选一个最靠谱的还有25%概率。多选则相反,宁可少选不要多选——很多多选是“漏选得一半分,错选不得分”,所以只选确定正确的选项,不确定的坚决不选。

编程题的时间分配更讲究。我的策略是先花5分钟把所有题目扫一遍,快速判断难度。这4道题里,我一眼能看出KMP和编辑距离是原题,就先把这两道做了拿稳分,再攻克任务调度,最后死磕拓扑排序。如果一上来就卡在最难的题上,后面简单的题反而没时间写,这是笔试的大忌。

另外提醒一点:笔试平台一般支持本地IDE调试,但ACM模式的输入输出处理一定要熟悉。很多同学LeetCode刷习惯了,不会处理while True加try except的循环输入,到了考场手忙脚乱。爱奇艺那场笔试就是ACM模式,第一题卡了好几个人在读入上。

4.2 高频失误与排查方法

复盘那场笔试,加上后来面试别人时看过的代码,我总结了几个算法工程师笔试中最常见的失误点。

数组越界是最基础的错误,多发生在for循环边界、两个字符串长度不一致、二维DP的初始化时。写代码前先确认边界条件是小于还是小于等于,空数组和单元素数组要不要单独处理,这些都要在心里过一遍。

数据类型溢出常常被忽视。很多DP题的状态值会很大,比如排列组合数量、最短路径长度,用int存可能溢出。笔试时如果没有特殊说明,尽量用long long或Python的int,避免因为溢出导致大case过不了。

时间复杂度不合格是更隐蔽的问题。任务调度这题如果写暴力模拟,小数据能过,但大数据一定超时。我建议每道编程题写完第一版后,用代码里的数据规模估算一下复杂度——如果n是10^5,O(n^2)就是10^10次操作,任何OJ都跑不完,必须寻找优化方案。

调试技巧方面,有一个非常实用:在本地测试时,自己构造几个边界case,比如空串、所有字符相同、n=0、图中有多个连通分量等。这些case往往是笔试平台的隐藏测试点,也是最容易出错的地方。我当时在编辑距离那道题上就吃了初始化的亏,后来养成了“写完先跑边界case”的习惯。

5. 算法工程师笔试备考经验

5.1 刷题方向的取舍

针对爱奇艺这类的视频平台算法岗,刷题方向可以分三层。

第一层是所有算法岗必考的基础题:数组、链表、栈、队列、哈希表、二叉树遍历。这些属于“热身题”,难度低但出现频率极高,不能失分。第二层是高频中等题:动态规划(背包、序列、区间)、贪心、二分搜索、DFS/BFS、字符串匹配。这些是编程题的主力,需要做到“见到题就知道用什么算法”的熟练度。第三层是进阶题:图论算法、并查集、线段树、树状数组、二分图匹配。这些考的概率低一些,但一旦考到就是拉分项。

数据结构和算法的经典题一定要亲手敲一遍代码,不要只看题解。我在备考时有个习惯:每道题先自己想思路,再看题解,然后关掉题解独立重写一遍。这样看似慢,但记忆深刻,考场上能直接“肌肉记忆”输出代码。

5.2 系统性的知识体系构建

机器学习基础是算法岗区别于开发岗的核心。面试时很多人算法题写得很溜,但一问到“L1正则化为什么产生稀疏解”“SVM的对偶问题是什么”就卡壳。建议按这个主线系统过一遍:

  • 特征工程:缺失值处理、标准化、离散化、特征选择
  • 经典模型:线性回归、逻辑回归、决策树、随机森林、GBDT、SVM
  • 模型评估:准确率、精确率、召回率、F1、AUC、ROC曲线
  • 优化算法:梯度下降、SGD、Adam、学习率调整策略

深度学习部分至少要把CNN、RNN、LSTM、Transformer的基本原理搞懂,尤其是激活函数、损失函数、反向传播的推导。这些知识点在客观题里占比很高,而且不像算法题那样可以临场推导,必须靠平时的积累。

5.3 针对视频平台的专业储备

如果目标就是爱奇艺这种视频平台,建议额外准备一些和视频业务相关的知识。比如视频推荐系统的整体架构(召回、粗排、精排、重排)、内容理解相关的视频分类和标签体系、弹幕和评论的文本挖掘方法、视频转码和分发中的资源调度问题。

我那时候因为提前研究过视频推荐,笔试时看到任务调度那道题,第一反应就是转码集群的任务分配——不同转码任务有依赖关系,有优先级,有资源限制,本质上就是图论和贪心的问题。这种“把业务场景抽象成算法模型”的能力,是算法工程师和普通开发最本质的区别,也是笔试和面试真正想考察的东西。

最后分享一个实用的技巧

笔试中如果遇到完全没思路的编程题,哪怕先写一个暴力解,也比留空强。暴力解至少能过部分测试点,拿到一部分分数。我那次拓扑排序的题,一开始只写了DFS的思路但没跑通,后来放弃调通,写了一版暴力遍历的O(n^2)解法,依然过了一些基础case。考场上每一分都很关键,千万别因为追求完美的解法而放弃保全分数。

另一个小技巧是:编程题提交前,花30秒审一遍代码里的输入输出格式。ACM模式下输出多一个空格、少一个换行,都可能导致零分。这是最亏的丢分方式,比不会做还让人难受。

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

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

立即咨询