
2024年秋招我参加了饿了么算法岗的第一批笔试时间定在8月中旬形式是牛客网双机位监控一共三道编程题加五道机器学习/深度学习基础选择题限时120分钟。整体难度偏中等偏上没有特别偏门的赛题但很考验基本功的扎实程度尤其是对经典算法的时间复杂度分析和调优能力。这篇文章我把当时记录的题目、我的解题思路、面试官后续面试中追问的问题以及复盘后觉得可以做得更好的地方都整理出来给后面要参加类似笔试的同学一个参考。1. 考前信息收集与笔试环境的几个细节先说点题外话。秋招第一批笔试的信息往往不会大面积公开很多同学是等收到邮件才临时抱佛脚但实际上从投递简历那一刻起就可以开始做准备。我当时是提前三天在牛客网上搜了饿了么上一年的笔经发现两个特点第一出题风格和美团、拼多多比较接近算法题偏工程实用场景很少出纯炫技的题目第二选择题部分对机器学习基础概念考察得很细不是简单背诵而是真的需要理解原理。笔试当天有几个环境细节值得专门提醒。双机位的要求是电脑摄像头拍正面手机微信小程序扫码作为第二机位放在侧后方45度角手机需要全程亮屏不能切出去否则会被记录异常。我提前试了一下座位角度确保手写草稿过程能清晰出现在侧机位画面里。另外牛客网的系统对Chrome浏览器的兼容性最好建议提前一天测好摄像头和麦克风权限。这些细节在真正笔试的时候非常影响心态我有同学就是因为第二机位角度不对被监考老师中途喊话调整白白浪费了五分钟。再说题目数量和时间分配。三道编程题加五道选择题满分100分编程题占75分选择题占25分。120分钟听起来不算紧但第三道题我实际写代码加调试花了将近50分钟所以前面两道题最好控制在35分钟以内选择题每题最多3分钟不会的果断跳过不要恋战。这不是说前面题简单而是说时间分配策略直接决定了你能不能把第三道拉分题做出来。2. 选择题部分机器学习基础考察的深度和广度五道选择题覆盖了模型评估、特征工程、经典算法原理和深度学习基础。单看知识点不算超纲但出题角度非常刁钻不是那种一眼就能看出答案的送分题。我挑几道印象最深的复盘一下。2.1 KL散度 vs. 交叉熵不是一个东西有一道题问的是在分类任务中使用KL散度作为损失函数和交叉熵作为损失函数的区别。很多同学第一反应是这俩等价啊但题目加了条件当预测分布存在零概率项时两种损失函数的梯度行为有何不同。这个问题的关键点在于KL散度包含预测分布的熵项即KL(P||Q) H(P, Q) - H(P)其中H(P)是真实分布的熵。在训练过程中真实标签分布固定所以H(P)是常数优化KL散度等价于优化交叉熵。但题目考的恰恰是H(P)在什么情况下不可忽略以及当Q中出现0概率而P不为0时交叉熵会直接产生无穷大损失而KL散度由于有H(P)作为缓冲项在某些实现中可以通过clip等技巧避免梯度爆炸。我当时选的是在softmax输出层使用logits而非概率时两者梯度形式一致这是从反向传播角度做的判断后来和同学对答案基本确认这个方向是对的。这道题给我们的备考启示是损失函数不能只背公式要能推导梯度表达式理解零概率带来的数值稳定性问题。面试中如果被问到你平时训练模型用哪个损失函数不要只回答交叉熵而要能说出在什么场景下需要额外处理数值稳定性的坑。2.2 KMP算法next数组的隐形考点选择题里有一道关于KMP字符串匹配的给的模式串正是网上流传比较广的abacaba要求计算next数组的某个特定值。这道题本身不难但凡手推过一次KMP的人都能做对但它出现在算法岗笔试的机器学习选择题部分体现的是出题方的一个隐藏思路算法岗的基础算法能力不能偏科数据结构和机器学习都要过硬。我在这里想多说一句next数组的推导逻辑因为网上很多教程next数组的定义不太统一有的next[i]表示当前字符不匹配时pattern应该回退到的位置有的表示前缀函数最长相等前后缀长度。考场上如果遇到一定要先看题目给的定义再代入计算。我记得abacaba的前缀函数是[0, 0, 1, 0, 1, 2, 3]下标从1开始如果next[i]定义为前缀函数值那结果就是确定的数字如果定义为回退位置需要在此基础上加1或减1非常容易错。建议备考同学专门花半小时把KMP的两种next定义都手推一遍并总结出它们之间的换算关系考场上一旦遇到就可以直接用不用现场推导浪费时间。2.3 排序算法稳定性和时间复杂度边界还有一道选择题考了排序算法在近乎有序的数组上的实际表现给了四组数据让选最优。这个考法比单纯背稳定性表更高级——它考察的是算法对输入分布的敏感性。比如插入排序在近乎有序数组上时间复杂度接近O(n)而快排如果每次选取的pivot都偏向两端最坏情况会退化到O(n²)。这道题的正确选项是数据近乎有序时优先选插入排序。原因在于插入排序的实际运行次数等于逆序对数量近乎有序意味着逆序对很少排序过程几乎只有比较没有交换常数系数极小。而堆排序虽然有稳定的O(n log n)上界但建堆过程的常数系数较大实际性能反而不如插入排序。考场上如果能想到逆序对数量决定插入排序开销这个层面基本就能快速锁定答案。这也提醒我们复习排序算法时不能只记时间复杂度的Big-O还要理解不同输入分布对实际运行时间的影响。2.4 粒子群算法和模拟退火的全局搜索差异关于粒子群算法PSO和模拟退火SA的对比题也很有意思。题目问的是在非凸函数优化问题中两者在跳出局部最优的能力和对超参数的敏感度方面有何差异。模拟退火的核心在于Metropolis准则即以一定概率接受更差的解这个概率随温度下降而减小。因为接受劣解的概率是逐渐变化的所以SA对温度下降速度退火计划非常敏感降温太快容易陷入局部最优降温太慢又浪费计算资源。粒子群算法则靠惯性权重、个体认知和社会认知三个参数平衡全局搜索和局部开发其优势在于粒子之间通过信息共享来交换探索成果种群多样性保持得更好但在高维空间容易过早收敛。这类题目在笔试中出现说明饿了么算法岗不仅仅要求会调包调用模型还要求对经典优化算法有原理级理解。因为我之前做过LSTM超参数搜索用的就是粒子群和贝叶斯优化对比实验所以这道题答得相对轻松。建议准备面试时一定要准备一个我用过模拟退火/遗传算法解决XX问题的具体案例面试官对这个话题有极高的追问热情。2.5 图像边缘检测的Sobel算子方向性最后一道选择题考了Sobel算子问的是水平方向边缘和垂直方向边缘的核矩阵对应关系。其实只要记住一个原则Sobel核是对高斯平滑和差分操作的近似水平方向边缘对应的是垂直方向像素变化明显的地方所以要用检测垂直梯度的Gx算子即左边一列为负数、右边一列为正数、中间为0的那个3×3矩阵。区分Gx和Gy其实是个小坑因为很多教程里说的是检测水平边缘用的是Gy水平方向差分这里水平指的是算子的方向而非边缘的方向容易绕晕。我当时在纸上画了一个一个小格子的亮度分布图才最终确认选对。这类图像处理的偏工程问题出现在选择题里算是饿了么的一大特色毕竟外卖平台有大量的图像处理需求比如菜品识别、OCR、安全检查等。3. 编程题第一题改版最长递增子序列稳定拿到基础分第一道编程题是给一个整数数组要求找出最长的非严格递增且相邻元素差值不超过k的子序列长度。这个题说是改编题其实内核就是一个动态规划加数据结构优化的问题。3.1 从最长递增子序列到有限差值的递推关系基础版本的最长递增子序列是经典的LIS问题状态转移方程是dp[i] max(dp[j] 1) for all j i and nums[j] nums[i]时间复杂度是O(n²)。加了相邻元素差值不超过k这个条件之后转移时需要限制nums[i] - nums[j] k。如果数组长度为10的5次方量级O(n²)必然超时必须优化。因为数组的值域是有界的我们可以在值域上维护一个数据结构来快速查询满足条件的dp最大值。当遍历到位置i时需要查询的是值在区间[nums[i] - k, nums[i]]范围内的所有dp[j]的最大值。这正好可以用线段树或树状数组来做。当时我选择的是离散化加线段树先把数组里所有可能出现的值排序、去重、映射成连续下标然后维护一个线段树每个叶子节点的含义是以该值为结尾的最长子序列长度查询时按值域范围做区间最大值查询更新时单点更新nums[i]位置的值为当前dp[i]。这样就做到了O(n log n)的时间复杂度。这里有个细节是严格递增和非严格递增的区别题目给的是非严格递增也就是允许nums[i]等于nums[j]所以区间查询的右边界取nums[i]本身而不是nums[i] - 1这个取值边界在严格递增和允许相等两种场景下非常容易搞混。3.2 线段树解法为什么比单调栈稳定可能有同学会想这题能不能用贪心加二分像普通LIS那样维护一个tail数组我试过不太可行。普通LIS的tail数组之所以能配合二分查找是因为tail数组内部天然是有序的而且后面接上的元素不影响之前元素的相对顺序。但加了差值不超过k的条件后tail数组的二分查找需要判断的不仅是大小关系还有差值约束这个约束会破坏tail数组的单调递增性质导致贪心失效。所以我直接选线段树理由是在线处理、天然支持区间查询、单点更新而且值域离散化之后空间复杂度是O(m)m是不同值个数完全可控。线段树虽然在代码量上比树状数组大一点但思维上更直观不容易出错。3.3 考场上的边界条件和坑这道题的坑主要在三个方面。第一数组可能包含负数离散化之前要注意偏移第二k可能是0这时候题目退化为找最长连续相等子序列容易漏掉空数组的情况第三当nums[i] - k算出来小于数组最小值时查询区间的左边界要取映射后的0而不是负数下标。我提交时第一次没通过用例就是因为k0时区间查询写成了比nums[i]小导致实际允许了严格小于的情况。笔试系统给出的错误反馈只告诉你是WA还是TLE不会告诉具体哪个用例挂了所以这种小边界只能靠平时的编码习惯来兜底。从策略上讲这道题属于基础题里面的进阶题如果你在考场上发现自己LIS的基本O(n²)写法都没把握就先写暴力版本能过一个测试用例是一个不要在优化上死磕。我当时是先写暴力版本自测了样例确认逻辑正确后再改写成线段树版本这样即使优化版本有问题至少能保证自己有可运行的代码兜底。4. 编程题第二题堆排序在Top-K场景下的灵活应用第二道题是个典型的Top-K变体给定一个很大的无序数组要求在不改变原数组的前提下找到第K大的元素。这个题如果直接说用快排的partition思想做快速选择平均时间复杂度O(n)是标准答案。但题目加了个限制要求使用堆排序的思想完成并且K不是常数可能非常大接近数组长度。4.1 为什么这道题的最优解是堆而不是快速选择我在之前的面试和笔试中反复被问到为什么Top-K问题往往用堆而不用快排partition这里可以做一个系统性的对比因为后面面试官大概率会追问。快速选择算法平均是O(n)最坏是O(n²)如果每次partition选择的pivot都偏向边界就会退化虽然可以通过三数取中法或随机化尽量避免但毕竟存在不确定性。堆排序建堆是O(n)后续每次调整是O(log K)在海量数据、内存有限、数据流式到达的场景下堆几乎是唯一合理的方案因为它只需要维护K个元素的空间不需要把所有数据加载进内存。不过这道题比较特殊的地方在于K接近数组长度时堆算法的时间复杂度会变成O(n log n)而快速选择仍然平均O(n)。我当时判断出题人想要的答案是构建一个小根堆当堆的容量超过K时弹出堆顶最后堆顶就是第K大的元素。因为题目明确要求使用堆排序的思想即使快速选择理论上更快也不能用。在笔试中遵循题目的约束远比追求理论最优重要。4.2 大根堆 vs. 小根堆的选择逻辑Top-K有个经典结论求第K大用容量为K的小根堆堆顶是这K个元素中最小的那个也就是所有遍历过的元素中第K大的反过来求第K小用容量为K的大根堆堆顶是所有遍历过的元素中第K小的。很多同学容易记反我提供一个记忆锚点堆顶总是离目标最近的层对于一个容量为K的小根堆堆内永远保存着当前看到的最大K个数而堆顶就是这最大K个数中最不最大的那个也就是第K大。考场上我还额外确认了一个细节数组里可能有重复元素重复值要正常入堆否则第K大在重复元素场景下会数错。比如数组[3,3,3,1]找第2大正确结果是3如果去重后再找就会得到1。这道题我当时通过了一次测试用了大概15分钟主要时间花在纠结要不要处理堆中重复元素最后保险起见选择了不主动去重的方案。4.3 实现细节和代码风格我给的代码是Java写的因为Java的PriorityQueue默认是小根堆直接就能用减少了很多手写堆的时间。但这里要注意Java的PriorityQueue的remove操作是O(n)如果需要在堆中删除任意元素来维护动态Top-K不要频繁调用remove而是要重新入堆一个元素再弹出堆顶。手写堆的版本对于C选手来说是家常便饭因为STL的priority_queue默认是大根堆求K大需要自己传greater比较器或者存负数绕一下。这两种写法在笔试中都能过关键是要非常熟练不要在主函数里debug堆排序的siftDown和siftUp那样时间绝对不够用。这道题做完我剩余的时间主要留着攻第三道题所以策略上是先把堆的20行核心逻辑写在草稿纸上再往编辑器里誊可以减少改错时间。5. 编程题第三题外卖场景下的最优路径规划综合考察图的建模第三题是整场笔试的压轴题分值最高也是一个典型的外卖业务场景题给定一个城市地图包含若干个取餐点和送餐点骑手需要从配送站出发依次完成所有订单的取餐和送餐最后回到配送站要求最小化总路程。每个订单有固定的取餐点和送餐点必须在取餐之后才能送餐且骑手一次最多携带两单。5.1 题目背后考察的核心算法状态压缩DP加最短路径初看以为是旅行商问题但加上一次最多携带两单和取餐先于送餐的约束后问题变成了一个带有依赖关系的路径规划问题。城市地图本身先要用Floyd算法预处理出所有关键节点之间的最短路径然后才是状态压缩DP的过程。因为它本质上是NPC问题但订单数量限制在12个以内这就明示了应该用状态压缩DP来解。我当时的建模思路是先跑一次Floyd算法得到所有点对间的最短距离这里的点包括配送站、所有取餐点、所有送餐点。Floyd的复杂度是O(V³)在V不超过100的地图规模下可以说毫无压力。然后枚举骑手当前的接单状态mask其中mask的每一位表示对应订单是否已经完成送餐同时记录当前所在位置cur。DP的转移是枚举下一步要执行的动作要么是去某个未取餐的订单的取餐点取餐前提是当前手上没有超过两单要么是去某个已取餐但未送餐的订单的送餐点送餐。5.2 状态定义和转移方程的推导过程因为骑手一次最多带两单所以单纯用哪些订单已完成作为状态还不够还要知道当前手上正在配送的订单是哪些。我定义状态为dp[mask1][mask2]其中mask1表示所有已经被取餐但尚未送餐的订单集合mask2表示所有已经完成送餐的订单集合。这里mask1和mask2的交集为空且mask1的size不超过2。也可以用三维数组dp[state][pickupMask][curPos]来写其中state是已经完成的订单bitmaskpickupMask是手上持有的订单bitmask。我当时用的是三维数组因为Java里二维数组的索引速度比用HashMap快很多。在转移的时候要注意取餐操作会让pickupMask增加一位同时产生一次路程代价从curPos走到取餐点送餐操作会让pickupMask减少一位同时让state增加一位路程代价是走到送餐点。因为一次最多带两单pickupMask的二进制中1的个数不会超过2这个约束在枚举转移时用Integer.bitCount判断即可。5.3 预处理最短路径Floyd比Dijkstra多源更合适这道题里面有三个以上的固定出发点和目标点如果用Dijkstra每计算一次单源最短路就要O(E log V)对每个起点都跑一遍会浪费大量时间在重复计算上而且Floyd无论从代码复杂度还是可读性看都更适合这种节点数小于200的稠密图。当时地图规模大概有60个节点Floyd跑一遍是O(216000)次循环几乎瞬间完成。有一个小优化是可以只跑Floyd获取关键节点间的最短路径不需要全图跑完存下所有节点对。因为DP阶段的转移只需要关键节点之间的距离非关键节点的距离对最终答案没有贡献。5.4 实际考场上我的思考和卡壳点这题虽然分值高但我考场上的思路还是没有完全落到代码正确性上。第一个卡壳点是状态压缩DP的初始化dp数组的初始值应该设为无穷大然后dp[0][0][startPos] 0其中startPos是配送站的编号。这个套路很常规但我在写的时候一度犹豫要枚举当前手上有没有单的初始状态后来想通了一点刚开始手上一定是空的然后从配送站出发去第一个取餐点这个过程不需要任何特殊处理。第二个卡壳点是如何判断一个订单已经取餐但未送餐也就是如何优雅地维护pickupMask。我最后的方案是state表示完成送餐的订单bitmaskpickupMask表示已取餐但未送餐的订单bitmask那么既没有取餐也没有送餐的订单就是全集中去掉state和pickupMask之后的剩余集合。每次从剩余集合中选一个订单去取餐时需要判断bitset中该位是否为0并且当前pickupMask的1的个数不超过1因为取完后会变成不超过2。写起来有几个if嵌套逻辑不难但非常容易漏判。第三个小坑是距离矩阵的不可达处理。Floyd算法如果两个节点没有路距离是INFDP转移时要跳过INF的路径否则会溢出导致错误结果。我一开始漏了这个判断跑本地小规模样例全对一交上去就WA后来加了一个dist INF / 2就跳过条件才通过。5.5 为什么这类题目会出现在外卖平台的笔试里如果只是从算法角度看这道题的核心就是状态压缩DP对懂套路的人来说并不十分稀奇。但题目把背景放在外卖配送场景下要求处理取餐点在送餐点之前最多携带两单这些业务约束实际上是在考察候选人能不能把真实业务问题抽象成数学模型。这种题目比纯做题更有区分度。我在面试环节被追问如果骑手最多能携带五单你的状态压缩DP还能用吗我的回答是状态空间会爆炸需要换用贪心加局部搜索等策略近似求解比如对取餐点做聚类或者用启发式搜索。如果备考同学时间充裕建议把这类业务约束加到经典算法题的建模思考中这是面试官最看重的可迁移能力。6. 笔试结束后的复盘与后续面试准备笔试结束后大概一周我收到了面试邀约。复盘整个笔试过程我觉得最值得记录的不是某一道题的解法而是考前信息筛选和考场时间分配的方法。如果你正在准备类似的大厂秋招笔试这几点我觉得是通用的第一是吃透经典题的边界而不是背模板。以KMP为例大部分人知道next数组怎么算但很少人真正思考过在不同的定义下next数组的数值会怎么变以及KMP在处理中文等多字节字符串时要注意字节边界还是字符边界。这些东西在选择题里非常容易成为拉分题。第二是算法题一定要在纸上和编辑器里双向练。我见过很多同学LeetCode刷了300道但笔试成绩不理想原因是他们习惯了LeetCode的本地调试和即时反馈不太适应牛客网这种只能提交一次的严肃场景。建议备考后期每周做一次限时模拟笔试而且要刻意练先写暴力再优化的习惯。因为笔试系统中部分用例可以通过暴力解法拿到一些分数有机会分就别空着。第三是算法岗的笔试内容越来越杂选择题里会出现图像处理、经典机器学习、分布式相关的冷门知识点。不要指望靠刷题App覆盖所有考点考前花一周时间把自己简历里提到的每个项目和课程知识点都过一遍基本概念远比背十个冷门面试题更有用。我当时就是把本科教材里的Sobel算子、卡尔曼滤波、粒子群算法的基本思想翻了一遍没想到真在选择题里派上了用场。最后再说一个小技巧笔试结束后如果系统允许尽量自己把代码拷贝下来或者用文字记录下自己的解题思路。很多公司的面试官在后续面试里会拿着你笔试时的代码记录来追问如果你自己都想不起来当时是怎么写的会给面试官留下很不好的印象。我在准备饿了么面试时就专门把第三道路径规划题的Floyd加状态压缩DP的过程重新梳理了一遍还画了状态转移图面试时这部分聊得特别顺畅。毕竟简历可以包装项目可以深挖但笔试代码是即时生成的它最能体现你真实的问题拆解能力认真对待笔试的每个环节本身就是最好的面试准备。