2019年春天那会儿,我正在准备找实习。简历投了一圈之后,收到了旷视科技算法研究员岗的线下笔试通知。说实话,看到"线下笔试"四个字的时候我是有点意外的,因为那个年头大多数公司为了省事都已经改成线上笔试了,旷视还坚持线下发卷子,而且是在学校里面租教室考,说明他们对候选人筛选这件事还是相当认真的。
整个笔试过程给我留下的印象挺深,不光是题目本身,还包括考试的组织方式、题目的出题思路,以及考完之后的复盘收获。这篇文章不打算写成"真题大全",毕竟具体题目我也记不全了,而且笔试题目本身每年都在变,照搬意义不大。我更想分享的是:旷视算法研究员笔试到底考什么、为什么考这些、线下笔试有什么容易被忽略的细节,以及如果你也想投这家公司的算法岗,该怎么准备才不至于踩坑。
1. 笔试概况与准备节奏:2019年春招线下的真实流程
1.1 投递与笔试通知
旷视2019年的实习生春招比我想象中启动得早。我记得是在2月底3月初的时候,公众号和牛客网上就开始挂出招聘信息,网申通道开了一段时间。我投的是算法研究员岗位,投完之后大概过了一周多,收到了笔试通知邮件。
这里有个细节值得说:邮件里明确标注了"线下笔试"和具体教室,还要求带身份证和学生证,以及一份纸质简历。我当时看到要求带纸质简历就觉得这家公司挺传统的,后来想想,线下笔试附带简历,可能意味着笔试成绩好的话,简历会直接递到对应部门面试官手里,流程会比纯线上筛选更高效。
线下的另一个好处是考场纪律严明,不太可能出现线上那种"你做完了帮我交一下"的操作空间。但代价就是,你必须亲自跑一趟,而且时间完全被锁死,不能像线上笔试那样挑自己状态最好的时间段去开考。
1.2 考前复习重点取舍
说实话,我收到通知到考试之间大概只有一周时间,能准备的东西有限。我当时做了个判断:旷视是做计算机视觉起家的,算法研究员岗笔试大概率会涉及机器学习、深度学习和基础算法三块,数学特别是概率统计和线性代数也不会少。
于是我把复习重点放在了四个方面:
- 数据结构与算法:排序、字符串匹配、树、动态规划,这些是最常考的基础。
- 机器学习基础:损失函数、正则化、过拟合、经典分类器原理。
- 深度学习基础:卷积神经网络、批归一化、激活函数、梯度消失问题。
- 数学基础:概率分布、极大似然估计、矩阵特征值、最优化方法。
后来考完回头看,这个复习策略大方向没错,但有个盲区——对优化算法相关的概念准备不足。那是后话,后面细说。
另外一个准备动作是:提前问了一下已经入职旷视的学长笔试的风格。他给的反馈是:旷视笔试不玩偏题怪题,题目覆盖面广但都比较基础,重点考察"是不是真的懂",而不是"背了多少"。他说的一句话让我印象很深:"他们不喜欢那种刷了几百道LeetCode但问个BatchNorm原理就懵的候选人。"
这句话直接影响了我后面几天的复习分配:我不再死磕hard题,而是花大量时间重新梳理机器学习深度学习的底层原理。
2. 算法与数据结构真题:从KMP到排序的实战拆解
2.1 KMP next数组:一道题暴露的细节功底
笔试第一道大题就是字符串匹配相关,具体题目是给一个模式串"abacaba",要求写出它的 next 数组。这道题出现在试卷比较靠前的位置,分值不算特别大,但我印象极深,因为它考察的是最容易被忽略的细节。
KMP 算法的 next 数组有多种定义方式,这是这道题真正的"坑"。如果教材用的是"最长相等前后缀长度"的定义,那么 next 数组是记录到当前位置为止,前缀子串的最长相等前后缀长度;如果用的是"失配跳转位置"的定义,通常会对长度做减一操作或者整体移位。不同的教材、不同的老师习惯不同,答案就会不一样。
我考场上是按"最长相等前后缀长度"来算的,那"abacaba"的前缀函数计算过程如下:
- 子串
"a":最长相等前后缀长度为0。 - 子串
"ab":前缀"a",后缀"b",不相等,长度为0。 - 子串
"aba":前缀"a"等于后缀"a",长度1;再看前缀"ab"和后缀"ba",不相等,所以最长长度为1。 - 子串
"abac":依次比较,前缀"a"和后缀"c"不相等,长度为0。 - 子串
"abaca":前缀"a"等于后缀"a",长度1,所以最长长度为1。 - 子串
"abacab":前缀"ab"等于后缀"ab",长度2,因此最长长度为2。 - 子串
"abacaba":前缀"aba"等于后缀"aba",长度3,所以最长长度为3。
所以按这个定义,next 数组是{0, 0, 1, 0, 1, 2, 3}。
备考的时候有个技巧:KMP 的 next 数组题目,千万不要只背代码模板,一定要自己动手写一遍求值过程,而且要同时掌握"前缀函数值"和"失配跳转位置"两种写法。考场上一旦你用哪种定义做题,就在卷面上把定义写清楚,这样即使跟标准答案不一样,阅卷人也能看出你是真的理解而不是瞎蒙。
2.2 排序算法对比:笔试中反复出现的"送命题"
排序在算法岗笔试里的地位就像是食堂的西红柿炒蛋——不一定在最显眼的位置,但基本顿顿都有。旷视这套卷子里也考了排序,不过问法比较有讲究,不是单纯让写快排代码,而是给了几段排序过程描述,要求判断分别属于哪种排序算法,并说明时间复杂度和空间复杂度。
这种题考察的是对排序过程"形态"的熟悉度。比如:
- 冒泡排序:每一轮相邻比较交换,最大的元素像气泡一样浮到末尾。如果描述中出现"相邻元素两两比较",多半是冒泡。
- 快速排序:选定pivot,然后把数组分成小于pivot和大于pivot两部分。如果描述中出现"分治""基准元素""划分",那就是快排。
- 堆排序:基于堆数据结构,反复把堆顶元素取出放到末尾。如果描述中出现"完全二叉树""堆调整",那就是堆排序。
- 归并排序:先把数组不断二分,然后两两合并有序数组。如果描述中出现"先拆后合""合并有序序列",那就是归并。
这里我整理了一下当年复习用的对比表,现在看依然很实用:
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | 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) | 稳定 |
一个容易忽略的点是快速排序最坏情况。很多人只记得平均复杂度 O(n log n),忽略了当输入已经有序且每次选择的pivot都是最值时,快排会退化到 O(n^2)。旷视笔试里就有一道相关的小题,问"待排序数组基本有序时,以下哪个排序算法效率最低",选项里有快排和插入排序。答案是快排,因为基本有序时插入排序效率高,而快速排序如果选pivot的策略不好,会频繁出现不平衡划分,递归深度接近n。
因此准备笔试时,别只背复杂度,要理解为什么复杂度是这样。比如快排的空间复杂度 O(log n) 来自递归栈的深度,归并排序的空间 O(n) 来自合并过程中借用临时数组,堆排序的 O(1) 空间是因为在原数组上做交换。理解了这些,不管题目怎么变,你都能答得上来。
2.3 手撕代码题的边界处理
笔试里还有一道手撕代码题,具体题目我记得是给一个整数数组和一个目标值,要求找出所有和为某个数的连续子数组。这类题目在LeetCode上很常见,但笔试的时候有一个特殊情况容易踩坑——数组里可能有负数和零。
如果数组全是正数,用双指针滑动窗口就行:右指针向右扩展,和超过目标值时收缩左指针,整个过程 O(n)。但一旦有负数,这个滑动窗口的单调性假设就不成立了,双指针会漏掉很多解,必须换成前缀和加哈希表的做法:先算前缀和数组 prefix[i] 表示前 i 个元素的和,然后遍历前缀和,用哈希表记录每个前缀和出现的次数,对于当前位置 i 的目标就是找到之前有多少个前缀和等于 prefix[i] - target,累加即可,时间复杂度 O(n)。
这个题本身不难,但考的是"你能不能识别出该用什么算法",而不只是把代码写出来。我当时先把边界情况列了一遍:数组为空、目标值为0、结果可能溢出、包含负数等等,然后再开始写代码。写代码的时候有个小细节帮我避免了不少麻烦——变量名不用 a、b、c 这种无意义的命名,而是用 prefix_sum、target、count 这种一眼能看懂的命名。阅卷人不一定会执行你的代码,但一定会读你的代码,写清楚比写简洁更重要。
3. 机器学习与深度学习考察:旷视作为CV公司的出题偏好
3.1 正则化与过拟合:原理题的高频考法
笔试的机器学习部分有道题问的是 L1 和 L2 正则化的区别。这题在机器学习面试里几乎就是"打招呼"级别的题,但旷视的考法多了一层——它给了一个场景:某个模型在训练集上准确率99.5%,在验证集上只有83%,问应该优先采取什么措施。
这里的核心是识别出过拟合,然后正则化就是顺理成章的答案。它接着追问:L1 和 L2 各有什么特点,在什么场景下选哪个。
L1 正则化是在损失函数上加权重的绝对值之和,L2 加的是权重的平方和。两者都能抑制过拟合,但原理和效果不同。L1 正则化会把部分权重推到0,产生稀疏解,相当于帮我们做了一次特征选择,适合特征维度很高且很多特征不重要的情况。L2 正则化则是把权重整体往小里压,但不会压成0,适合特征维度适中、认为所有特征都有点用的情况。
从优化角度理解会更清楚:L1 的惩罚项在0点不可导,所以梯度下降迭代时,一旦权重靠近0,就会被"推"到精确的0上;L2 的梯度是线性的,权重小的时候梯度也小,很难精确到达0。
我当时在卷子上把这个区别从几何角度解释了一遍:在二维权重空间中,L1 的约束区域是菱形,L2 的约束区域是圆形。损失函数等值线和约束区域相切的位置,菱形更容易在坐标轴上相切,因此产生稀疏解;圆形几乎不可能相切在坐标轴上,所以权重通常不是0。用图形解释不仅简洁,而且比光背结论更能体现理解深度。
3.2 批归一化:细节决定成败
卷子里有一道关于 Batch Normalization 的题,考察的点非常细致:训练时和推理时,BN 层用的均值和方差分别来自哪里?
这道题看似基础,但确实能刷掉一批只背过答案没深究过的人。训练阶段,BN 对每个 mini-batch 计算当前批次的均值和方差,然后用它归一化当前批次的数据;推理阶段,没有固定的 mini-batch 概念,用的是训练过程中累积的全局统计量——通常用滑动平均的方式维护的 running mean 和 running variance。
难点在于为什么推理时必须用滑动平均而不是直接用当前输入算出来的统计量。我当时的理解是:推理时输入往往只有一条样本,单条样本的均值和方差没有任何统计意义,用它做归一化会引入很大的噪声。滑动平均在训练过程中不断累积,反映的是整个训练数据集的分布特征,这样推理时才有稳定的参照。而且 BN 在训练时还引入了两个可学习参数 γ 和 β,推理时这两个参数也是固定的。
再往深一步,BN 为什么能加速训练?有几种解释,最被广泛接受的是它缓解了内部协变量偏移,让每层输入分布更稳定,因此可以使用更大的学习率,收敛更快。此外 BN 还有一点隐性的正则化效果,因为每个 batch 的统计量有随机波动,相当于给网络加了点噪声,这会让训练过程不那么容易过拟合。
答这种题的时候我的经验是:不要只写结论,要把"训练和推理不一致的原因"写出来。面试官看的是你有没有真正理解设计动机,而不是单纯记住结论。
3.3 损失函数的设计逻辑
机器学习部分还有一道题考了交叉熵损失。题目给了几个常见的损失函数表达式,要求识别哪个是交叉熵,并解释它为什么适合分类任务。
交叉熵损失的标准形式是:
L = -Σ y_i * log(p_i)
其中 y_i 是真实标签的one-hot编码,p_i 是模型预测的概率分布。用它来做分类任务的核心原因是:它直接衡量真实分布和预测分布之间的差异,并且和 softmax 配合得非常好——softmax 先保证输出归一化成概率,交叉熵再衡量概率分布的匹配程度,两者结合能让梯度的计算变得非常简洁。
如果从最大似然的角度看,最小化交叉熵等价于最大化训练数据的对数似然,也就是说,交叉熵损失的背后有一个很自然的概率解释:我们希望模型给真实类别赋予的概率尽可能高。
还要留意一个细节:多分类任务中,如果真实标签是 one-hot 形式,交叉熵损失其实只关心真实类别对应的那个概率值,因为其他位置 y_i=0,相乘后是0。所以表达式可以简化为 L = -log(p_c),其中 c 是真实类别。这意味着交叉熵损失实际上是在惩罚"模型对正确类别的不自信",预测概率越接近1,损失越小;越接近0,损失越大,而且当 p_c 接近0时,损失是趋向无穷大的——这对训练来说是一种强烈的信号。
3.4 深度学习基础题:CNN参数量计算的陷阱
笔试中出现了一个卷积神经网络参数量计算的题目,看起来不算难,但很容易算错。给的条件大致是:输入是三通道的 224×224 图像,第一层卷积用的是 64 个 7×7 的卷积核,stride=2,padding=3,要求计算该层的参数量。
很多人会直接算 64 × 7 × 7 = 3136,但这只算了二维卷积核的权重,漏掉了输入通道数。正确的计算方式是:每个卷积核的权重是 输入通道数 × 7 × 7,再加上1个偏置,然后乘以输出通道数。也就是:
(3 × 7 × 7 + 1) × 64 = 148 × 64 = 9472
输出特征图的尺寸其实不影响参数量,因为它只是权重在空间上的共享。这个"参数共享"是CNN的核心思想之一,也是CNN参数量远小于全连接网络的根本原因。无论输入是 224×224 还是 1024×1024,只要输入通道数和卷积核尺寸不变,卷积层的参数量就不变。笔试里容易坑人的点就在这里:题目故意给了很多输入尺寸信息,暗示你可能需要算特征图尺寸,但实际上参数量的计算根本用不到。
反过来,如果题目问的是"计算量和FLOPs",那特征图尺寸就有用了,因为计算量等于参数量乘以输出特征图的尺寸。这种情况下需要根据输入尺寸、stride、padding算出输出特征图的高宽:
output_size = (input_size + 2×padding - kernel_size) / stride + 1
代入数字:(224 + 2×3 - 7) / 2 + 1 = 223 / 2 + 1 = 112.5。这不是整数,说明实际网络里在 stride=2 的卷积之前通常会先做下采样或者调整尺寸。这类题出的数字往往故意不是整数,目的就是考察你算完之后能不能发现输入输出尺寸不匹配的问题,从而想到实际设计时可能需要额外的填充或裁剪操作。
4. 数学与优化算法:粒子群这类"冷门"考点背后的逻辑
4.1 概率统计题:从贝叶斯到极大似然
概率统计在算法研究员笔试里的地位很稳固,几乎每套卷子都会涉及。旷视这张卷子里也有几道相关的题,包括一道贝叶斯公式的简单应用题,以及一道要求写出极大似然估计推导过程的题。
贝叶斯公式本身不难,核心公式就是:
P(A|B) = P(B|A) × P(A) / P(B)
笔试关键是理解每个量的含义,以及题目给的条件该往哪里套。贝叶斯公式在机器学习里的体现就是:后验概率 ∝ 似然 × 先验概率。这个视角在面试中经常会被引申,比如问你:"为什么贝叶斯估计比极大似然估计更稳健?"答案就在于它引入了先验,当数据量少的时候先验能起到约束作用,避免过拟合。
极大似然估计那道题我记得是要求对一组服从正态分布的样本做参数估计。推导过程并不复杂:写出似然函数,取对数,对均值求导令其为0,解出样本均值;对方差求导令其为0,解出样本方差。但有个细节是:极大似然估计得到的方差是有偏的,分母是 n 而不是 n-1。这个点如果能在卷子上主动指出来说明你懂的东西比题目要求的多,在阅卷时会有额外的加分效果。
4.2 粒子群算法:为何出现在算法研究员笔试里
笔试里有一道题我准备时完全没料到,就是粒子群算法的原理。题目给了一段粒子群算法的迭代描述,要求补充速度和位置更新的公式。
粒子群算法(Particle Swarm Optimization, PSO)是一种群体智能优化算法,灵感来自鸟群觅食行为。每个粒子代表解空间中的一个候选解,粒子有位置和速度,位置就是候选解各维度的取值,速度决定了它下一步怎么移动。迭代过程中,每个粒子记住自己历史最优位置 pbest,整个群体共享全局最优位置 gbest,然后根据这两个信息更新速度:
v = w×v + c1×r1×(pbest - x) + c2×r2×(gbest - x)
x = x + v
其中 w 是惯性权重,控制粒子保持原来运动趋势的程度;c1 和 c2 分别是自我认知和社会认知的学习因子,通常取2左右;r1 和 r2 是 [0,1] 之间的随机数,用来引入随机性。
我当时在考场上看到这道题的第一反应是:这不是进化计算的内容吗?怎么算法研究员笔试也考这个?后来复盘才想明白,这其实代表了旷视对算法研究员的一种期望——他们希望你不只会调深度学习框架,还要对"优化"这件事本身有广泛的理解。深度学习训练本质上也依赖优化算法SGD、Adam,粒子群作为一种不需要梯度的全局优化方法,在超参数搜索、网络结构搜索等场景里也有应用。一个合格的算法研究员,不该只把自己局限在纯梯度优化的框架里。
这道题给了我一个特别重要的教训:准备算法研究员笔试,别只看深度学习那点内容,传统的启发式优化算法、概率图模型、基础数值优化方法都值得扫一遍。考的概率可能不高,但一旦考到,对别人来说是盲区,对你是送分题,这就是差距。
4.3 线性代数与矩阵运算
线性代数考了一次矩阵特征值的计算,还有一个矩阵求导的基本题目。特征值计算本身不难,都是基础操作,但一旦矩阵维度变大计算就很容易出错,我的建议是:拿到题先看一下矩阵的结构,如果能写成对角块矩阵或者三角矩阵,特征值直接读出来就行,少走很多弯路。
矩阵求导那道题跟深度学习的联系很紧密,因为反向传播本质上是链式法则加上矩阵求导。比如给定损失函数 L = ||Wx - y||^2,要求对 W 求梯度。展开之后就是:
L = (Wx - y)^T (Wx - y)
对 W 求导得到 2(Wx - y)x^T。这个结果在最小二乘问题里非常常见,其形状是 d×d,和 W 的维度一致。笔试里考这种题,一方面考矩阵求导基本功,另一方面也是直接为后续的神经网络梯度推导做铺垫。
一个提升矩阵求导能力的小方法是记熟几个常见规律:二次型对向量求导、线性变换对矩阵求导、以及链式法则在矩阵层面的应用。尤其是最后一点,反向传播的数学本质就是链式法则,如果能用矩阵形式把梯度表达清楚,面试手推梯度的时候会非常有优势。
5. 线下笔试的节奏把控与踩坑经验
5.1 时间分配策略
旷视这套笔试题量不算小,我印象中选择题、填空题大概有30多道,后面还有几道大题,总考试时间是90分钟。拿到的第一件事肯定是快速翻一遍整张卷子,对题目难度有个整体判断。
我当时的策略是:先把有把握的题一次性做掉,难题用笔做上记号,最后再回来啃。选择题和填空题快的5分钟内能过完,遇到犹豫超过2分钟的题果断跳过——因为前面的基础题错过才是真的亏,后面的大题做不出来不丢人。
时间分配上,我给自己定的目标是:选择题加填空题控制在35到40分钟以内,剩下的时间全部留给大题。这样做的好处是,就算后面的大题来不及完全写完,至少前面拿分的部分保住了。
不过这里要提醒一个线下笔试特有的心理因素:卷子放在面前,你会发现旁边的人翻页很快,这时候很容易产生焦虑感。我的经验是压根不要观察别人,因为人家翻页快可能只是跳过了不会做的题,不代表他拿的分比你多。专注在自己的卷子上才是唯一正确的事。
5.2 机试与纸质卷的差异
线下笔试并不都是纸质卷子,旷视这次是先在机房做了一轮机试,再发的纸质卷。机试环节用的是类似牛客网的在线评测系统,代码题提交后立刻判分。
机试的题目我记得有两道,一道是数组相关的模拟题,另一道是图论相关的最短路径问题。前一道比较轻松,把题意搞清楚、注意一下边界条件就能过。后一道最短路径如果直接用 Dijkstra 算法就能解,但数据范围给得比较大,用邻接矩阵存图会超内存,必须用邻接表加优先队列优化。
这个环节其实很考验代码熟练度,因为在线评测系统不会给你"写个思路就行"的机会,要么通过要么不通过。备考时一定要多练手写代码,尤其是直接在空白编辑器里写代码的能力——没有IDE的自动补全、没有编译提示,所有东西都要靠自己。平时在LeetCode、牛客网上刷题时尽量别依赖自动补全,养成在无辅助环境下写代码的习惯,等到考场上就不会慌。
5.3 复盘:哪些题不该错
考完之后我给自己做了一轮复盘,客观来说有几道题是不该错的。
第一道是KMP的next数组。题目考的其实就是定义和计算,但我当时因为纠结"题目里的next数组到底按哪种定义"浪费了不少时间。现在回头看,与其纠结不如直接在卷面上写明自己的定义再往下算。这算是考场心态问题,平时做模拟题的时候就应该有意识地练习"面对歧义题目时怎么处理"。
第二道是正则化那题,L1和L2的区别我背得很熟,但题目给了具体的过拟合场景,我一开始差点只顾着写定义,差点忘了结合场景分析。笔试做题和面试答题有个共同点:不要太快动笔,先花半分钟把题目读完,把需求拆清楚,再组织答案。
第三道是粒子群算法,完全没想到会考。这道题只能说是准备范围的疏漏,不算技术问题。吃一堑长一智,从那以后我准备任何算法岗笔试,都会把启发式优化算法、传统机器学习、概率图模型这些"非深度学习"的内容也过一遍。
5.4 给后来人的备考清单
结合这次笔试经历,我整理了一份算法研究员笔试备考清单,如果目标是旷视这类以计算机视觉为核心业务的AI公司,以下内容值得逐一过一遍:
- 数据结构与算法:排序全部,字符串匹配至少掌握KMP和BM,二叉树遍历、最近公共祖先、树的直径等常见树问题,动态规划从背包到区间DP都要熟悉,图的最短路径(Dijkstra、Floyd、Bellman-Ford)要会手写。
- 机器学习:线性回归的闭式解和梯度下降推导、逻辑回归损失函数推导、SVM的拉格朗日对偶、决策树的信息增益和基尼系数、随机森林与GBDT的区别、正则化原理、过拟合的处理方法、模型评估指标(准确率、精确率、召回率、F1、AUC)。
- 深度学习:反向传播的矩阵形式推导、常见激活函数的优缺点、BatchNorm和LayerNorm的区别、常见损失函数(交叉熵、MSE、Hinge Loss)的适用场景、CNN各层参数量和计算量的计算、RNN和LSTM的梯度问题。
- 数学基础:贝叶斯公式、极大似然估计、常见分布(正态、伯努利、泊松)及其性质、特征值和特征向量、正定矩阵、矩阵求导、拉格朗日乘子法、梯度下降的各种变体原理。
- 优化算法:SGD、Momentum、RMSProp、Adam的原理和区别,粒子群、模拟退火、遗传算法等启发式优化方法的思想,凸优化的一些基本概念。
这份清单不一定覆盖所有公司所有方向的考点,但覆盖面已经足够广。算法研究员的笔试本质上是考察"数学基础、机器学习功底、算法工程能力"三者的综合水平,任何一块短板都可能成为被筛掉的理由。
笔试结束差不多两周后,我收到了面试通知。回看整个准备过程,最大的体会是:笔试题目看起来杂,但核心就是在检验你是不是一个有广度也有深度的算法工程师——广度是指基础知识的覆盖面,深度是指对每个知识点是否真正理解到"为什么"。如果只刷题不思考,选择题或许能蒙对,但大题的推导和场景分析一定会露馅。保持这个标准去准备,不管考的是旷视还是其他AI公司,都能拿出有底气的表现。