2019年那阵子,自动驾驶赛道正是最热的时候,Momenta作为国内头部玩家,技术岗笔试的含金量和筛选强度都不低。我当时投的是算法岗,拿到题的瞬间第一反应是“这真的是笔试吗”而不是“这题我会不会”——因为它的考察维度很杂,除了常规的数据结构和算法手撕,还有不少数学推导、深度学习原理和C++底层的硬核问题。如果你是准备自动驾驶方向校招的同学,这套题型的思路至今都有参考价值。
这篇文章把我的做题经历、考点复盘、典型题解法以及踩过的坑整理成一份可以“抄作业”的备考笔记。无论你投的是感知、规划、控制还是基础架构方向,这套笔试题背后隐藏的能力模型是共通的:基础扎实、代码干净、能快速把算法问题抽象成可实现的工程方案。
1. 内容整体设计与思路拆解
1.1 Momenta技术岗笔试的底层逻辑
先说结论:Momenta 2019秋招技术岗笔试不是单纯考刷题量,它更像一张“能力雷达图”,覆盖四个维度——数学基础、算法与数据结构、深度学习原理、C++/Python工程能力。为什么这么设计?因为自动驾驶公司要的人,不是只会训练模型的人,而是能落地的人。
感知团队需要你懂图像处理的底层逻辑,甚至要会手推反向传播;规划团队需要你快速写出稳定的路径搜索算法;基础架构团队需要你写出高性能、无内存泄漏的C++代码。一套笔试题同时筛这四类能力,靠的就是不同模块的组合。我记得当时题型分布大致是这样:
| 模块 | 题型 | 主要考察点 |
|---|---|---|
| 数学与概率 | 选择题+计算题 | 矩阵、概率分布、期望方差 |
| 算法与数据结构 | 编程题 | 链表、二叉树、动态规划 |
| 深度学习 | 简答+手推 | 卷积原理、反向传播、优化器 |
| 工程基础 | 代码阅读+填空题 | C++内存、多线程、Python特性 |
1.2 它解决的核心问题:筛选“能做事的人”
笔试只是第一关,但它是性价比最高的一关。面试可以看你思考方式,但笔试能在两小时内快速判断你是否具备基本盘。我记得当时考完和一个朋友聊,他算法题全AC了,但C++那几道指针题几乎全空,最后挂在了流程里。不是说算法不重要,而是对自动驾驶技术岗来说,工程能力本身就是技术能力的一半。
这套笔试的另一个隐性逻辑是“压力测试”。两个小时的限时,题目量大概在10-15道之间,包含选择题、填空题、简答题和编程题,时间非常紧张。设计者的意图很明确:在没有足够时间反复纠结的情况下,你暴露出来的就是你平时最真实的技术习惯。写完这道题,你平时有没有认真抠过TensorFlow的卷积实现,有没有自己动手写过智能指针,有没有关注过内存对齐——全部一览无余。
1.3 为什么这套题值得反复研究
我不是让你去找2019年的原题背答案,而是让你理解这套题背后的出题思路。这几年我看过不少自动驾驶公司、大厂和AI独角兽的笔试题,万变不离其宗。你搞懂了Momenta这套笔试为什么这么出,再看其他家你会发现很多题目都是同一个套路。
比如深度学习考察,很多公司只会问“BatchNorm的作用是什么”这种概念题,但Momenta更喜欢让你推导一个卷积层的参数量、FLOPs,然后反推感受野。这种题目没有标准答案,却能区分“背过八股文”和“真懂原理”的人。这种出题方式,后来成了很多自动驾驶公司笔试的模板。
2. 核心细节解析与实操要点
2.1 数学与概率:不只是公式,要会算
数学部分我印象深刻的有几类。第一类是矩阵相关的计算,比如给你一个变换矩阵,问它对一个点做旋转和平移之后的坐标是多少。这类题在SLAM、传感器标定里非常常见,所以基本是必考。第二类是概率和统计,比如“抛一枚不均匀硬币,正面概率p,求连续抛出n次至少出现k次正面的期望”之类的变形题。
这类题的核心不是记公式,而是理解随机变量的分解技巧。我后来总结了一套自己的方法:遇到期望类题目,先试着把复杂随机变量拆成若干简单变量之和,用线性期望去算。这种做法几乎能解决90%的期望题。比如二项分布的期望是np,很多同学直接背,下一次换成超几何分布就懵了。你可以把每个位置看作一个01变量,期望是p,整个序列的期望就是np,这个思路其实比背公式更快更稳。
实操建议:准备期别只看凸优化和概率论的目录,一定要动手算,算到看到一个分布能条件反射写出它的期望、方差、矩母函数为止。笔试现场不允许你慢慢回忆公式。
2.2 算法与数据结构:高频题型的“肌肉记忆”
算法部分是硬骨头。Momenta 2019那场,编程题大概是三道左右,考查重点集中在链表、二叉树、动态规划、排序和二分查找这几个大类。我记得有一道链表题是要在O(nlogn)时间内完成排序,很多同学第一反应就是快排,但链表不支持随机访问,所以正解是归并排序的链表版本。
这种“经典题变种”是笔试的常态。它们考验你在限制条件下灵活调整算法的能力。我建议你别只刷LeetCode的高频题,多练习这些变种:
- 链表:反转链表、链表排序(归并)、删除倒数第N个节点、判断环
- 二叉树:层序遍历变种、最近公共祖先、二叉树转双向链表
- 动态规划:背包变形、最长上升子序列、编辑距离、股票系列
- 二分查找:变种很多,比如找旋转数组最小值、找峰值、二维矩阵搜索
具体到代码实现,要养成一套固定习惯。比如链表题,先考虑要不要引入dummy节点;二叉树题,先想是递归还是迭代,空间复杂度有没有限制。这种习惯在笔试现场能帮你减少大量debug时间。
2.3 深度学习:原理性与工程性并重
深度学习部分是我觉得最“自动驾驶特色”的模块。不单纯问你“什么是注意力机制”,而是要把结构和数学原理揉在一起考。我记得有题问的是卷积操作在特征图上的感受野计算,另一题是关于BatchNorm的训练与推理阶段区别,还有一题是让推导一个两层网络的反向传播过程。
我当时最大的感受是:如果只是用过PyTorch、TensorFlow写模型而不理解底层数学,这部分的题基本拿不到分。比如感受野计算,不是说你会调卷积核大小和步长就行了,你得理解每一层输出特征图上一个像素对应原图的区域大小。这个在目标检测、分割模型的设计里是基本功。
给个简单的公式:对于连续卷积池化层,输出特征图每个元素对应的感受野大小和所有层的卷积核大小、步长、池化大小都有关系。笔试里常见的考法是给你一个网络结构表,让你算最后一层的感受野。我建议你手动推一遍LeNet或者VGG的感受野增长过程,推完整个笔试的“卷积计算题”就稳了。
2.4 C++工程基础:藏在细节里的陷阱
C++部分对没有扎实底层功底的算法同学来说杀伤力最大。我印象很深的是几道关于智能指针和多线程的题。比如“shared_ptr是否线程安全”、“weak_ptr如何解决循环引用”、“移动构造和拷贝构造的区别”,以及“vector的扩容机制底层是怎么实现的”。
这些题看上去都是八股文,但真正写过大规模C++项目的人会发现这些细节会直接影响程序稳定性。自动驾驶车辆上的感知、规划模块通常是7x24小时运行,任何内存泄露、野指针、数据竞争都可能造成严重后果。所以面试官问这些,其实是在判断你有没有“硬工程能力”。
我给你的建议很直接:笔试前把《Effective Modern C++》里的关键章节过一遍,尤其是智能指针、移动语义、lambda表达式、多线程同步这几块。并且一定要自己动手写几个小例子去跑,只看书是记不住的。
3. 实操过程与核心环节实现
3.1 典型编程题与完整解题思路
下面我完整复盘几道我当时遇到的或类似的题目,附上思路和代码,这个过程比题目本身更有价值。
题目1:实现一个LRU Cache
要求get和put操作的时间复杂度都是O(1)。这是缓存淘汰策略里的经典题目,也是相机制作和调度系统里很常用到的结构。底层结构需要哈希表+双向链表:哈希表保证O(1)查找,双向链表保证O(1)插入和删除。
#include <unordered_map> struct Node { int key, value; Node* prev; Node* next; Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {} }; class LRUCache { private: int capacity; Node* dummyHead; Node* dummyTail; std::unordered_map<int, Node*> dict; void removeNode(Node* node) { node->prev->next = node->next; node->next->prev = node->prev; } void addToHead(Node* node) { node->next = dummyHead->next; node->prev = dummyHead; dummyHead->next->prev = node; dummyHead->next = node; } public: LRUCache(int cap) : capacity(cap) { dummyHead = new Node(0, 0); dummyTail = new Node(0, 0); dummyHead->next = dummyTail; dummyTail->prev = dummyHead; } int get(int key) { if (dict.find(key) == dict.end()) return -1; Node* node = dict[key]; removeNode(node); addToHead(node); return node->value; } void put(int key, int value) { if (dict.find(key) != dict.end()) { Node* node = dict[key]; node->value = value; removeNode(node); addToHead(node); } else { Node* node = new Node(key, value); dict[key] = node; addToHead(node); if (dict.size() > capacity) { Node* last = dummyTail->prev; removeNode(last); dict.erase(last->key); delete last; } } } };这道题在笔试现场容易犯两个错误:一是忘记处理capacity为0的极端情况,二是删除节点后没有同步清理哈希表。前者会导致空指针访问,后者会导致内存泄漏。在写代码之前,先和面试官或阅卷系统确认边界条件,这是一种很好的工程习惯。
题目2:最大连续子数组和(Kadane算法变种)
LeetCode 53的变种,几乎每场技术笔试都会出现。基础版本用动态规划,状态转移方程是dp[i] = max(nums[i], dp[i-1] + nums[i]),结果取dp数组的最大值。空间可以压缩到O(1)。
int maxSubArray(std::vector<int>& nums) { int currentSum = nums[0]; int maxSum = nums[0]; for (int i = 1; i < nums.size(); ++i) { currentSum = std::max(nums[i], currentSum + nums[i]); maxSum = std::max(maxSum, currentSum); } return maxSum; }笔试中这题常见变种有两个。一个是要求输出子数组本身而不只是最大和,这时候需要维护起止指针。另一个是要求数组可以循环,即子数组可以首尾相接。后者是LeetCode 918,做法是先算不循环的最大子数组和,再算循环情况下的最大和(等价于总和减去最小区间子数组和),两者取较大值。建议自己推一遍循环版本的边界条件,笔试现场很可能遇到。
3.2 深度学习手推题的复现过程
反向传播手推是笔试里比较让人紧张的一部分,因为你不仅要写出结果,还要展示清晰的计算过程。我建议用一种固定的流程来写:
- 画计算图,标出每个节点的输入输出。
- 设中间变量,分别命名为z1、a1、z2、a2,避免长表达式。
- 从输出层往前逐层求梯度,每写一步都标注“这对应链式法则的哪一项”。
以一个两层的全连接网络为例,输入维度是2,隐藏层维度是2,输出维度是1,激活函数是sigmoid,损失函数是均方误差。推导时重点关注链式法则中不同路径的梯度之和。很多同学会在中间层梯度那里漏掉“来自后一层两个路径的梯度相加”,这是最典型的丢分点。
我的心得是,把反向传播当作“递归复合函数求导”,而不是“矩阵运算公式”。前者不容易出错,后者一旦矩阵维度对不上就全盘崩。笔试前至少手推三遍:单隐层全连接、CNN的单个卷积核、Softmax+交叉熵。这三类覆盖了90%的考题场景。
3.3 C++代码阅读与内存布局分析
有一类题很迷惑人,就是给你一段有内存问题的C++代码,让你找出问题并说明后果。典型的问题包括:返回局部变量的引用、数组越界、new后没有delete、shared_ptr循环引用。这里我给一个我在做题时常用的检查顺序:
- 检查返回值类型,尤其是返回引用或指针的情况
- 检查所有new/delete、make_shared的配对情况
- 检查for循环边界,尤其是有等号还是没有等号
- 检查类是否有虚函数,如果有,类成员的布局和大小计算
- 检查多线程下共享变量的访问是否需要加锁
遇到“vector扩容后迭代器失效”的题,我的回答套路是这样的:vector在push_back导致扩容时,会重新分配一段更大的内存,然后把旧的元素拷贝或移动过去,此时指向旧内存的迭代器全部失效。失效的原因是旧内存已经被释放,继续使用就是悬垂指针。想扩展一下的话,可以提reserve提前分配,也可以比较deque和list在这方面的不同,这种补充会显得你理解更系统。
4. 常见问题与排查技巧实录
4.1 笔试时间不够用怎么办
我自己经历过两次“时间不够用”的考场状态。第一次是太追求完美,一道算法题写完了还反复优化,导致后面的深度学习简答题只能草草写几句,分数自然不好看。第二次学乖了,先花三分钟把所有题目浏览一遍,把每道题的预估时间写在草稿纸上,超时就先跳,绝不纠缠。
给一个通用的时间分配策略:选择题和填空题每道不超过3分钟;简答题和手推题每道8-10分钟;编程题每道15-20分钟。程序题如果20分钟还没有完整AC,就先把暴力解法写上,保证能过一部分测试点,然后注明优化方向。这种策略不是投机取巧,而是一种工程资源管理能力。阅卷人更愿意看到你有策略地拿分,而不是在某一道题上钻牛角尖导致大面积空白。
4.2 熟悉的题目突然AC不了
笔试现场最容易崩的一种情况是:明明刷过类似的题,但换了个条件就写不出来了。比如求“最长回文子串”的题,变成“最长回文子序列”,解法就从中心扩展变成了动态规划。这种问题不是你不会,而是刷题的时候只记了“最优解”而没理解“这个解法为什么对”。
我在备考后期采用了一个方法:每做完一道题,强迫自己写一行“核心难点”和一行“解法动机”。比如做“旋转数组找最小值”,核心难点是“有序数组被旋转后依然是部分有序”,解法动机是“利用二分查找和断点的位置关系”。这个习惯让我在笔试时遇到变形题也能快速定位到对应的“解法动机”,而不是机械背诵模板。
4.3 代码跑通但内存出错
不少同学笔试时用的是在线OJ环境,写C++时不小心犯内存错误却调试不出来。我遇到过最典型的情况是:链表的删除操作里,删除最后一个节点后没有把新的尾部节点的next置空,结果遍历输出时死循环。
面对这种情况,我的建议是:编码时脑子里始终有一份“内存关系图”,每操作一次指针,就确认一下它指向哪里。尤其是写链表、二叉树这类指针密集型的题目,多花10秒检查next和prev的赋值顺序,能省下后面30分钟的debug时间。另一个实用技巧是,在所有循环体结束处检查“当前节点的前驱和后继是否都指向预期位置”。这好比开车看后视镜,虽然多一个动作,但能防止很多大问题。
4.4 深度学习概念题拿不准怎么办
遇到拿不准的概念题,一个小技巧是把你知道的所有相关内容分层写出来。比如题目问“Batch Normalization的作用”,你不要只写一句“加速收敛”,而是展开成三层:训练时它对小批量数据做标准化,减少内部协变量偏移;推理时使用全局统计量而不是当前批次的统计量;它对初始化缩放不敏感,从而可以用更大的学习率。阅卷人最怕看到一句话答案,如果你能展示“我知道原理、知道它在训练和推理阶段的区别、知道它的工程影响”,即使某个细节记错了,整体印象也会好很多。
碰到完全没见过的题,比如“你知道1x1卷积的作用吗”,也可以从降维、增加非线性、实现跨通道信息融合三个角度去答。哪怕是猜测,也要展示出“我在主动构建知识体系”的能力,而不是被动的“背题机器”。
5. 备考方向与复习资源整理
5.1 核心复习清单
我在那次考试后,把自己踩过的坑和被问到的知识点做了一个复习清单,这里直接分享给你。如果你正在准备类似公司的技术岗笔试,可以按这个清单横向排查:
- 数据结构:数组、链表、栈、队列、哈希表、二叉树、堆、图的基本操作和复杂度
- 算法思想:二分、双指针、滑动窗口、回溯、动态规划、贪心、并查集
- 数学基础:线性方程组与矩阵运算、特征值与特征向量、概率分布、参数估计、常见不等式
- 深度学习:反向传播手推、卷积与池化的原理、常见激活函数与损失函数、Dropout与BatchNorm
- 工程语言:C++内存模型、左值右值与移动语义、智能指针、进程线程与并发;Python多进程、装饰器、生成器
别想着一次全看完,这个清单的价值在于查漏补缺。你可以在每周复习结束时对着这个清单问自己“这周我更新了哪一行的认知”,逐项打勾,比漫无目的地刷题效率高得多。
5.2 用什么方式练脑最有效
刷题不是越多越好。我见过刷了800道LeetCode但笔试依然不理想的同学,问题出在“练习模式太单一”——只在IDE里写题,只在题解区看答案,很少模拟真实考试的紧张感。我的经验是考前一周安排至少三次完整的限时模拟,题目就用各家公司往年的笔试真题,电脑开成飞行模式,手机放另一个房间,强迫自己在两个小时内像正式考试一样答题。
模拟结束后不要只看分数,要逐题分析:哪道题卡了超过20分钟、哪道题的边界条件漏了、哪道题的C++知识点是真的不会。把这些问题记到笔记本上,考前最后一天只看这个笔记本。这里有一个容易忽略的细节:限时模拟时要习惯使用考场提供的本地编译器,而不是你常用的IDE。因为在线OJ的编译警告处理和本地IDE不同,早暴露这些差异,考场上就不会被奇怪的重定向问题卡住。
5.3 从笔试到面试的能力迁移
虽然这篇文章重点是笔试,但我想强调一点:笔试题里暴露的短板,面试时一定会被继续追问。比如你笔试里C++内存题没答好,面试官大概率会在技术面里专门准备一道更深入的智能指针题目来判断你是不是“补课”了。因此备考时不要抱“蒙混过关”的心态,真要搞懂每个知识点。
我当时因为笔试里一道深度学习手推题写得不够完整,面试时被一直追问到“如果中间层的激活函数换成ReLU,反向传播的梯度形式怎么变”。还好我备考时真的手推过ReLU的导数,但如果不准备,这一连串追问肯定会露馅。所以我建议你用“笔试倒逼复习,面试倒逼深化”的思路,把笔试当作面试的预演,而不是一个孤立的关卡。
6. 写在最后的体会
回看2019年那次笔试,我最大的收获不是“拿到Offer”,而是知道自己哪块能力是虚的。C++工程部分当时有多道题我靠直觉蒙对了,但心里清楚如果不补上,就算入职也只能做一个调包侠。后来我把智能指针、移动语义和内存序相关的书认真读了一遍,练了几十个工程场景的小例子,才真正觉得“自己的手是干净的”。
如果你正在准备类似企业的技术岗笔试,我想说的是:这套题确实不简单,但它考的东西都是将来工作中每天都要用的。与其说是在筛人,不如说是在给你画一份“工程师能力地图”。你花两周时间把地图上的空白区域涂满,收获的不仅是一场笔试的通过,更是从学生思维到工程思维的一次升级。祝准备笔试的同学顺利,有具体问题也欢迎在评论区交流,我看到会尽量回复。