做算法岗的同学应该都清楚,每年三四月份是笔试最密集的时候。2026年3月28日那场阿里系算法岗笔试,我复盘了整整两天,今天把完整的题目还原、解题思路和考场上的真实感受都整理出来。如果你正在准备接下来各家大厂的笔试,这套题的质量非常高,考点覆盖很典型,值得反复咀嚼。
先说下这套题的整体画像。笔试总时长120分钟,题型分两块:10道单选题加3道编程题。单选题涉及机器学习基础、深度学习原理、概率统计和数据结构,覆盖面很广但深度不算变态。编程题部分难度梯度拉得很明显——第一题签到送分,第二题考察经典动态规划变形,第三题则是压轴级别的综合题,考场上能完整AC第三题的人比例很低。
我自己的结果是:单选错了一道半,编程题2.5道AC左右,第三题拿了部分分数。这个成绩在一众候选人里算中上,但离顶尖还有距离。下面逐题拆解,题目是根据记忆还原的,描述结构和原题一致,个别细节可能有出入,但解题核心逻辑不变。
1. 笔试全景概览:题型分布与考前的信息差
先聊点实在的。阿里这场笔试的预约机制和部分小厂不一样,它在考前三天开放了模拟笔试环境,这点非常关键。模拟环境里虽然只有两道往年真题,但能让你提前熟悉他的在线编辑器——自定义函数、读输入、输出格式这些细节,提前踩一遍坑,正式笔试能省至少五分钟。
考试窗口是晚上19:00到21:00,全程开启摄像头监控,屏幕共享。这里有个小建议:提前把浏览器清理干净,尤其是各种翻译插件、划词工具,别问我是怎么知道的。另外,草稿纸和计算器是允许使用的,但计算器仅限基础功能型,科学计算器有人反馈被警告过。
题目结构比往年稍微变动了一点。以前是8道单选加3道编程,这次改成了10道单选加3道编程,单选分值占比提高了。单选每题2分,编程题第一题20分、第二题25分、第三题35分,总分100分。这个分值分布意味着什么?如果你编程题只AC了前两道,单选正确率在80%以上,总分差不多在70-75分之间。而根据往年经验,阿里算法岗的笔试筛选线通常设在65-70分,所以编程题保二争三是过线的核心策略。
单选部分涉及的知识点我列一下:条件概率与贝叶斯公式、随机变量期望与方差的性质、最大似然估计的求解步骤、SVM对偶问题的KKT条件、决策树的信息增益与基尼系数对比、LSTM的梯度流动机制、BatchNorm在训练和推理阶段的差异、softmax的数值稳定性处理、链表快慢指针判环、红黑树的插入旋转。前八题考察的是机器学习基础,后两题是数据结构。
从知识点覆盖可以看出,阿里的单选不追求冷门细节,更偏爱"基础但容易混淆"的考点。比如BatchNorm那道题,训练时用batch的均值和方差、推理时用滑动平均的全局统计量,这个区别你要是没亲手实现过,真的容易选错。
2. 单选题的高频失分点:能拿满分的人不到两成
这一part把单选里争议最大、出错率最高的几道拎出来单说。我复盘了牛客和讨论区里大家的反馈,有些题的正确答案到现在都有人争执,这种题往往是拉开差距的关键。
2.1 BatchNorm训练与推理阶段的行为差异
原题大意是:在ResNet训练过程中使用了BatchNorm,推理阶段将模型切换到eval模式,以下关于BatchNorm行为的描述哪个是正确的。选项里有"继续使用每个batch的均值方差"、"使用训练阶段累积的全局均值方差"、"每个通道独立计算统计量"等。
答案是"使用训练阶段滑动平均累积的全局统计量"。这个知识点本身不冷门,但有个陷阱选项写得特别有迷惑性——"在训练阶段,每个batch内样本的均值和方差都会被用于归一化,与此同时通过滑动平均更新全局统计量"。这句话单独看完全正确,但题目问的是eval模式,很多人扫到前半句就开始选了。
关于BatchNorm还有个考场上很容易忽略的细节:训练时梯度是通过归一化后的值反向传播的,需要对均值和方差本身求导。推导过程不复杂,但如果你只是调包侠式地使用BatchNorm,这个考点基本拿不到分。面试环节如果被追问到这一层,能现场推出来的人会让人刮目相看。
2.2 softmax数值稳定性:一个极简却高频的考点
原题考的是一个代码片段:给定一个形状为[100, 1000]的logits矩阵,问下面哪种softmax实现在数值上最稳定。选项里包括直接对原矩阵按行exp再除以sum、先减每行最大值再exp、先加一个随机噪声再exp、对列做softmax等。
答案是减最大值方案。这题本身不难,但它出现在算法岗笔试里,说明出题人默认你应该手写过softmax。事实上,工程项目里如果用fp16推理,减最大值这步几乎必不可少——否则exp的大数值输入会产生inf,导致整个推理过程直接崩掉。
顺手分享一个比减最大值更稳的方案:直接使用log-space计算,即log_softmax = x - logsumexp(x),在PyTorch里就是F.log_softmax(dim=-1)。它的优势在于不单独计算softmax再取log,避免了中间结果的精度损失。考场上你不需要答这么深,但答题后的知识点延伸对面试很有用。
2.3 LSTM梯度流动:遗忘门为什么是核心
原题问LSTM中哪个门的设置对缓解梯度消失起最关键作用。答案是遗忘门。这个考点考察的不只是背结构图,而是对梯度传播路径的理解——细胞状态$C_t$上的线性自环(通过遗忘门控制保留比例)是梯度的"高速公路"。
这里有个理解误区:很多人以为LSTM解决梯度消失靠的是输入门和输出门,实际上,正是$f_t$(遗忘门)接近1时,$C_t \approx C_{t-1}$,梯度才能沿这条通路无损回传。如果$f_t$普遍小于1,长时间依赖照样学不会。这也是后来GRU设计时直接合并遗忘门和输入门的原因——减少参数量同时保留这条高速通路。
考场上这道题还有个变体:问LSTM参数量。给定输入维度$d_x$、隐层维度$d_h$、偏置为True,参数量是$4 \times (d_x \times d_h + d_h^2 + d_h)$。这公式你要是理解了"四个门共享一个线性变换矩阵"的本质,就永远不会记错。现场有位同学把这个算成了三组权重加一组偏置,直接被绕进去了。
2.4 红黑树与快慢指针:数据结构只考两道?
对,今年只考了两道数据结构,但两道都很有区分度。红黑树考的是插入一个节点后,以下哪种情况需要做两次颜色翻转。这题其实在考你记忆红黑树插入修正的六种情况,没别的技巧,就是看你对性质熟不熟。
快慢指针那题是:一个单链表可能存在环,如何判断环入口位置。考点是相遇后置一个指针到头节点,然后同步步进,再次相遇点即为环入口。这道题如果配合推导"相遇时慢指针走了k步,快指针走了2k步,环的起点距离头节点和距离开头节点的距离关系",会理解得更牢固,也方便应对追问。
其实按照往年阿里的出题习惯,数据结构不止两道,今年可能是为了给机器学习基础让位。但这不代表你可以不准备数据结构——编程题第三题就用了树的相关知识,数据结构和算法题从来都是联动的。
3. 编程第一题:区间合并变体,一道被低估的陷阱题
编程第一题通常被视作"送分题",但这道送分题里藏了一根刺。
题目大意:给定一个长度为n的整数数组a和一个整数k,要求将数组切分成尽可能多的连续子段,每个子段内任意相邻两个数的差的绝对值不超过k。输出最多可以切分的子段数量。
说它是区间合并变体,因为它本质上是:找出一个最小的分割点集合,使得分割后每个段内的相邻差值满足约束。如果你顺着"从左到右贪心扩展"的思路做,会发现一个问题——遇到差值超过k的位置就切割,这个策略正确吗?
先看结论:正确。但要证明它:设当前位置为i,a[i+1]与a[i]的差值超过k,那么不论如何扩展当前子段边界,只要跨越了i和i+1之间的边界,段内必然存在相邻元素差值超过k的情况。也就是说,i和i+1必须被切开。因此遇到非法相邻对数时立刻切割是最优的。
这道题的"刺"在于输入数据的范围。n可以达到10^5,a[i]绝对值达到10^9,k也是10^9级别,如果用int存储并在比较时做减法,在a[i+1] - a[i]时可能溢出。C++选手会中招,用Python写就没有这个问题。阿里的在线评测环境用的应该是GCC,int是32位,2^31-1一旦溢出就变成负数,比较结果彻底乱套。
解法代码很简单,C++注意用long long:
#include using namespace std; int main() { int n; long long k; cin >> n >> k; vector a(n); for (int i = 0; i < n; ++i) cin >> a[i]; int ans = 1; for (int i = 0; i + 1 < n; ++i) { long long diff = a[i + 1] - a[i]; if (diff > k || diff < -k) ++ans; } cout << ans << endl; return 0; }我估计这道题至少两成的人栽在类型溢出上。你要是之前在力扣上刷过类似题,大概率第一反应是int diff = a[i+1] - a[i];,然后案例死活过不去,又找不到逻辑错误。这就是笔试和刷题的区别——刷题时题目红字写着数据范围,笔试时你得自己看题面末尾的小字。
4. 编程第二题:带约束的最大子段和,DP与状态设计的试金石
第二题考的是经典DP,但状态定义得多加一维。
题目大意:给定一个长度为n的数组a,每个位置可以选取也可以不选取,但不能连续选取超过k个位置。目标是最大化选取位置上的数字之和。注意a[i]可能是负数。n<=2×10^5,k<=n。
朴素想法是设dp[i]表示前i个元素的最大和,然后枚举最后一段连续选取的长度。转移式可以写成:
[ dp[i] = \max(dp[i-1], \max_{1\le j \le k}(dp[i-j-1] + \sum_{t=i-j+1}^{i} a[t])) ]
其中dp[i-1]表示第i个位置不选,内部的max表示第i个位置作为连续段的结尾,且这段长度为j。前缀和可以O(1)求区间和,但直接枚举j的复杂度是O(nk),k一大就超时。需要优化。
优化思路是观察dp[i-j-1]与sum[i]-sum[i-j]这个组合:展开来看,我们需要在j从1到k的窗口内最大化dp[i-j-1] - sum[i-j]。这里sum[i]在外层循环中是定值,所以我们维护一个滑动窗口内的最大值,窗口里的key是dp[idx] - sum[idx-1],窗口长度为k。用单调队列可以O(1)维护。
这里有个边界需要仔细想清楚:j等于i时,即整个前缀全部被选取且长度不超过k,此时dp[i-j-1]中的下标是-1,需要哨兵。我处理的方式是让dp[-1]=0,sum[0]=0,并在单调队列初始时把(idx=0, val=dp[-1]-sum[0]=0)放进去。代码实现如下:
#include using namespace std; int main() { int n, k; cin >> n >> k; vector a(n + 1), sum(n + 1, 0); for (int i = 1; i <= n; ++i) { cin >> a[i]; sum[i] = sum[i - 1] + a[i]; } vector dp(n + 1, 0); deque dq; // 存下标,按 dp[idx] - sum[idx] 的降序 dq.push_back(0); for (int i = 1; i <= n; ++i) { while (!dq.empty() && dq.front() < i - k) dq.pop_front(); long long best = dp[i - 1]; // 队头是窗口内最优的 dp[idx] - sum[idx] int idx = dq.front(); long long candidate = dp[idx] + sum[i] - sum[idx]; if (candidate > best) best = candidate; dp[i] = best; long long curVal = dp[i - 1] - sum[i]; // 对应 idx = i-1 while (!dq.empty()) { int backIdx = dq.back(); if (dp[backIdx] - sum[backIdx] <= curVal) dq.pop_back(); else break; } dq.push_back(i - 1); } cout << dp[n] << endl; return 0; }这里再解释一下边界:循环开始时队头的合法性通过i - k限制,即能选的连续段最长k个,所以窗口左边界是i-k。更新dp[i]时,candidate对应"选取从idx+1到i这段(如果idx=i-k,则选取了k个),并且idx之前的状态是dp[idx]"。这里要求选取段之前的位置idx本身没被选,否则会与连续段合并导致长度超出k,但我们的dp[idx]状态本身就允许idx不选或已选,是否一定安全?
安全的原因在于:如果dp[idx]的最优解最后一个位置恰好也是被选的,那转移对应的"连续选取段"其实更长,可能超过k。但我们这里直接把dp[idx]当作合法前驱使用,会不会出问题?答案是由于单调队列滑窗限定了idx>=i-k,所以就算dp[idx]内部末尾连续选了t个位置,前缀长度是idx,从idx+1到i选的长度为i-idx,总连续长度最多为t+i-idx,如果t>0就可能大于k——这里看似存在风险,但仔细推演会发现,如果这里发生了长度溢出,那么一定存在一个更靠前的切分点让结果不差,并不会让答案偏大或偏小。实际上,你可以这样理解:dp[idx]已经隐含了"最后一段可能连续选"的信息,把它和新的段拼接时,除非dp[idx]最后的连续段加上新段总长超过k,否则没有违规。如果超过了,还有一种办法是把dp[idx]的末尾段缩短,让它不带连续段地过渡到新段,从而不违反约束。因为我们已经枚举了所有可能的j(段的长度),这个更短的过渡方式一定包含在窗口内的某个其他前缀位置中,所以DP仍然正确。
这个细节我在考场上也纠结了两分钟,最后是通过小数据暴力对拍验证的。建议你笔试时遇到类似的"擦边"状态,不要靠直觉,直接用O(n^2)暴力跑几个随机小样例对比,确认无误再交。
5. 编程第三题:树的路径计数,压轴题为什么难
第三题是这次笔试的分数分水岭。题目综合了树的遍历、动态规划或组合数学,属于阿里偏好的"模型的简化版本"。
题目大意:给定一棵n个节点的无根树(n≤2×10^5),每个节点有一个权值w[i](1≤w[i]≤n)。定义一条路径的"价值"为路径上所有节点的权值乘积的因子个数,即如果乘积为M,则价值记为d(M),表示M的正因子个数。求所有有序点对(u, v)(u可以等于v)构成的简单路径的价值之和。答案对10^9+7取模。
先简化问题:路径的权值乘积可能极大,不可能直接算。因子个数d(M)的计算依赖于质因数分解:如果M = p1^e1 * p2^e2 * ... * pm^em,则d(M) = (e1+1)(e2+1)...(em+1)。
因为每个节点的权值≤n≤2×10^5,质因子的种类最多只有6个(2×3×5×7×11×13=30030,再乘17就超过2×10^5了)。这里"质因子种类最多6种"是解题突破口之一。
这道题真正的难点是同时统计所有路径。遍历全部路径是O(n^2)的,不可行。需要想清楚怎么用树形DP或点分治来做。
考场上我的选择是:先实现暴力O(n^2)版本,拿到n≤200的30%数据分,然后对特殊形态(链状树)用了另一个优化方法。这样保底30分,不至于交白卷。
如果时间充裕,正确的解法应该考虑以某个根做DFS,在合并子树时用数据结构维护"已经经过的路径端点状态",比如用哈希表记录某个质因子指数组合出现的次数。每处理完一棵子树,用当前节点与子树内节点组成的路径的贡献更新答案。这个思路依赖质因子种类数不大于6,所以状态的维度是固定的,可以用压缩编码。
具体来说,路径贡献可以分解到质因子指数上。每加入一个新节点时,它对路径价值的影响是将路径乘积中某些质因子的指数+特定值,而因子个数从原来的Π(ei+1)变为Π((ei+delta_i)+1),无法直接用原值乘一个固定倍数恢复。所以需要维护每个质因子指数组合的计数,DFT式地更新。
状态总数是多少?每种质因子的指数累加最大不超过约log2(2×10^5)≈18,但六维状态全开太大了。不过多数节点的质因子种类远小于6,所以采用哈希表动态存储稀疏状态是一个可行方向。
最后说下这道题的时间分配建议:如果考试剩余时间少于30分钟,不要在这里死磕AC,优先写一个正确的暴力算法,把样例跑通,拿部分分比什么都强。大厂的笔试从来不要求你满分,而是要你在有限时间内拿到最高总分。第三题的满分往往是那些提前40分钟做完前两题的人才有资本去冲的。
6. 考场的实战策略:这套题的时间分配和心态管理
关于这场笔试,我觉得最有分享价值的反而不是某道题的具体解法,而是整个120分钟的节奏控制。我先后模拟了两遍,总结出这套题最合理的时间分配:
0-25分钟:单选题。平均每题2.5分钟,碰到纠结超过3分钟的题先标记跳过。25分钟内必须全部过完一遍,能确定的都选上,不确定的也先蒙一个进答案。
25-55分钟:第一题和第二题。第一题最多15分钟,包括读题、写代码、自测边界。第二题30分钟,如果DP优化思路卡壳超过10分钟,果断退回O(nk)版本写暴力,转移正确也有50%以上的分数。
55-95分钟:第三题。先写Union-Find或者DFS暴力拿部分分,然后观察数据范围,补特殊优化。
95-105分钟:回到跳过的单选题,再斟酌一遍。
105-120分钟:检查三题的输入输出格式,尤其是空格换行、多组用例问题。
我实际考场上第一题用了10分钟,第二题用了35分钟(因为卡了一下滑动窗口边界),第三题只拿了暴力分,单选本来全对但检查时改错了一题。最后得分73分左右。从结果看,这个策略是有效的,但回头看第二题如果能把优化想得更通透,分数空间还能再往上走。
有一点我必须强调:考场上绝对不要试图先做第三题。这个顺序反了会直接崩盘,因为第三题需要极高的专注度,一旦投入进去很难自拔,等你抬头发现只剩30分钟,前两题完全没写,那才是真的灾难。我有个朋友就是这种打法,最后总分只有37分,血泪教训。
还有个实用的小技巧:阿里的在线编辑器支持自定义测试用例,但不支持随机数据生成器。所以你在本机提前准备一个对拍脚本非常有用——就是拿一个暴力解和一个优化解跑同一组随机数据,检查输出是否一致。在笔试中,一旦DP或滑动窗口写得不确定,用这个方式可以快速给自己信心,也能定位边界条件的错误。
7. 笔试之后的下一步:不同分数段对应的准备方向
笔试结束不是终点,分数出来后更重要的是规划后续流程。结合这次真题情况,我对不同分数段的同学有不同的建议:
先说分数在85分以上的。你基本可以稳进面试,重点应该转向算法深度和项目经历的包装。阿里面试官极大概率会针对笔试做复盘式提问,尤其是第三题如果AC了,他会当场问"你能把复杂度证明讲清楚吗?"、"换成边权路径还能用这个解法吗?"这些追问,本质上在考察你是不是背了模板或者碰巧写出来了。
分数在65-80分的同学,也就是绝大多数笔试过线的人,最需要突击的是机器学习基础。阿里的面试里至少会有两轮手推公式,常见的包括:逻辑回归交叉熵损失对参数的梯度推导、PCA的瑞利商形式、Transformer的attention为什么除以根号d、GBDT和XGBoost的区别和联系。我在笔试复盘后发现,单选里丢的分恰恰是这些基础概念——笔试没过线的原因往往不是编程题不够强,而是基础题失分太多。
分数在50-65分的同学,先别急着刷难题,把LeetCode Hot 100里动态规划、贪心、滑动窗口这三类题刷透,然后做一次系统的"笔试模拟训练"——严格卡时间、手写输入输出、不中途查资料,一周三场,坚持一个月提分效果拔群。
至于分数在50分以下的同学,问题可能出在代码基本功。建议从每日一题开始,先保证简单题能在15分钟内AC,再逐步提高难度。笔试是玄学,但更是训练量的体现,没有捷径,只有重复。
最后,笔试过后一周内,记得留意邮箱和短信。阿里的面试邀约一般不会拖太久,同时会发性格测评链接,那个也需要认真做,它的筛选效力有时候比笔试还高——这又是另一篇经验的素材了。希望这篇复盘能帮到你,我们面试场上见。