提到“在线编程题”,我猜不少经历过校招的研发工程师都不陌生。2016年那会儿,美丽联合(后来大家更熟悉的名字是蘑菇街、美丽说)校招笔试里,在线编程题就是一道绕不过去的坎。很多同学平时LeetCode刷得飞起,一到笔试平台就翻车,要么卡在输入输出上,要么栽在复杂度上,还有人是被多组测试用例活活折磨到超时。我后来自己也参与过几轮技术招聘的笔试出题和阅卷,再回头看当年的笔试,很多门道才真正想明白。这篇东西,我就把在线编程题这件事从头到尾聊透:它到底想考什么、拿到题怎么分析、有哪些坑每年都有人踩,以及备考阶段怎么准备才有效率。
它不是那种“背几道题就能过”的应试技巧总结,而是把出题人、阅卷人和做题人三个视角放在一起讲。不管你是正在准备校招的应届生,还是想跳槽的社招同学,哪怕只是好奇大厂笔试怎么筛人,这篇内容应该都能给你一些参考。
1. 在线编程题到底在考什么:一道题背后的三层逻辑
1.1 从“会写代码”到“会解决问题”的跨越
很多人觉得在线编程题就是考算法,其实不完全是。面试官想在四十分钟到一个小时里确认的事情,至少包含三个层次。
第一层,是编码基本功。你能不能把脑子里的思路,快速转成一段语法正确、逻辑完整的代码。这一层看起来简单,实际是筛掉人最多的环节。本地IDE写代码习惯了自动补全和编译报错提示,到了在线编辑器里裸写,很多人连include该写什么、Scanner和System.in哪个快都要犹豫半天。
第二层,是算法与数据结构能力。给你一个没见过的题目,你能不能识别出它背后的经典模型。是排序?是二分?是动态规划?还是图论里的最短路?这一层考验的是知识迁移能力,见过足够多的题型,才能在看到题目时快速定位到对应的解题框架。
第三层,是工程意识。代码是否清晰、变量命名是否有意义、边界条件有没有考虑周全、有没有处理异常输入。笔试虽然主要靠自动判题机跑结果,但很多公司会人工抽查高分段代码,代码风格如果太糟糕,到了面试环节会被面试官拿来讲事。
这三层,对应的分别是“能写”“会想”和“做得好”。在线编程题不是单纯考你会不会背某几个算法,而是通过一个限时、限环境、限性能的场景,把这三个层次的真实水平一次性测出来。
1.2 电商公司为什么偏爱这类题
像美丽联合这种电商背景的公司,笔试题目往往有个特点:题干会带点业务包装。比如“满减凑单”“优惠券组合”“库存分配”“购物车结算”,看起来像是业务需求文档,实际剥开外壳,内核全是经典算法。
为什么这么设计?因为研发工程师入职后要面对的真实业务,本质上就是把业务问题抽象成技术问题。一个订单系统要算最优优惠组合,就是在做动态规划或贪心;一个推荐系统要筛选候选集,就是在做TopK或排序;一个库存系统要分配发货仓,就是在做二分图匹配或贪心策略。笔试出题人很自然地把这些业务场景封装成了算法题。
所以说,在笔试里看到那种题干特别长、还带着电商业务背景色的题目,不要慌。先把业务词汇圈出来,比如“满减门槛”“每位顾客最多买一件”“总价超过阈值”,然后问自己一个问题:如果不看这些业务描述,这题的数学模型是什么?把业务包装剥掉,剩下的就是数据结构与算法的老朋友们。
1.3 阅卷视角:面试官到底在看什么
我参与阅卷时有个很深的感受:在线编程题不是“AC了就万事大吉”。
自动判题机会告诉你这道题通过还是没通过,但出题人会看整套试卷的分数分布。一道题如果有超过一半的人没做出来,说明题出难了;如果一小部分人秒过,说明题可能出简单了。更重要的是,高分的源代码会被人工抽查,尤其是那些通过时间特别短、代码量特别少的提交。
人工看代码的时候,我最在意的是两件事。第一,代码是不是“硬凑”出来的。比如有人在循环里套了无数个if去枚举特殊情况,这种代码即使AC了,也只能说明边界写得够多,不代表抽象能力好。第二,代码能不能被别人看懂。变量名是a、b、c,函数几百行都不拆,注释一个没有,这种代码到了面试现场,让本人来讲都可能讲不清楚。笔试不是终点,面试官拿着你的笔试代码做追问,才是很多公司的常规操作。
2. 动手前的准备:环境、基本功与复杂度意识
2.1 笔试平台的三个隐形规则
在线笔试和本地写代码完全是两回事。哪怕你刷了几百道LeetCode,第一次上笔试平台,还是可能被环境规则坑到。
第一个规则是语言和编译版本。很多笔试平台默认支持的编译选项和最新版IDE不一样,C++可能只支持到C++11,Java可能是老版本,Python也可能停留在某个中间版本。开考之前,先把环境说明看清楚,确认你准备用的语法特性在对应版本里能不能用。我见过有人用了C++17的if constexpr特性,本地编译通过,线上直接编译失败,整整一道题白扔。
第二个规则是输入输出。在线判题机器是按测试用例来评分的,每个测试用例都通过标准输入读数据,再把结果写到标准输出。如果题目要求处理多个测试用例直到文件结束,而你的代码只读取了一组数据就退出,后面全部分数都拿不到。
第三个规则是提交次数和罚时。有的平台限制每道题的提交次数,比如最多十次,每次提交失败都会影响最终排名。所以不要拿第一版代码就去试错,先在自己本地把逻辑、边界、极端情况都测一遍再交。
2.2 输入输出处理:最容易翻车的环节
输入输出翻车,是笔试里最冤的丢分方式,不是不会做,而是数据根本没读进去。
Java里,Scanner写起来方便,但在数据量大的时候性能极差,会直接导致TLE。正确做法是用BufferedReader加StringTokenizer,甚至手写一个快速的输入解析类。C++则是cin默认要和stdio同步,不关同步的话,大数据输入会有明显性能损耗,所以很多人一上来就写ios::sync_with_stdio(false); cin.tie(nullptr);。Python的问题也一样,input()在多层循环里会拖慢速度,直接改用sys.stdin.buffer.read()整体读入再解析,性能要好很多。
还有一个容易被忽略的点:多组输入的结构。有些题目会先给一个整数T表示测试用例数量,然后后面跟着T组数据;有些题目不给T,而是要求一直读到文件末尾。这两种写法不一样,建议把两种输入模板都提前准备好,考试时直接套模板,把精力留给算法本身。
2.3 复杂度估算:落笔之前先算一笔账
很多同学写代码不先算复杂度,凭感觉写完一交,判题机直接TLE,才发现算法选错了。落笔之前,先看一眼数据范围,估算一下你的算法在最坏情况下要跑多少步,这是程序员的基本素养。
我通常按下面的参考来反推可接受的复杂度:
| 数据规模 n | 可接受的复杂度 | 举例 |
|---|---|---|
| n <= 10 | O(n!)、O(2^n) | 暴力全排列、子集枚举 |
| n <= 20 | O(2^n)、O(n * 2^n) | 状态压缩动态规划 |
| n <= 100 | O(n^3) | Floyd、三重循环 |
| n <= 1000 | O(n^2) | 双层循环、常规DP |
| n <= 10^5 | O(n log n) | 排序、二分、线段树 |
| n <= 10^7 | O(n) | 单次线性扫描 |
| n >= 10^8 | O(log n)、O(1) | 数学公式、快速幂 |
这个表格只是一个粗略参照,实际还要看常数因子和平台性能,但至少能做到一件事:写代码之前,你先知道自己这条路能不能走得通。如果数据范围是10^5,你准备写O(n^2)的暴力,那就不用写了,大概率超时。
3. 核心题型拆解:研发岗最常考的4类题
3.1 数组与字符串:白板题的基本盘
数组和字符串是出现频率最高的题型,因为几乎所有公司都默认你牢牢掌握它们。
这类题的核心套路其实就那几个:双指针、前缀和、滑动窗口、排序加二分。双指针能解决有序数组的两数之和、三数之和、去重等问题;前缀和可以把区间和查询从O(n)降到O(1);滑动窗口处理连续子数组的最长最短问题特别顺手;排序加二分则是很多"找满足条件的对数"类题目的通用解法。
我建议把这些套路整理成模板,跟着同类型的题目反复练。比如最长无重复字符子串用滑动窗口、连续子数组最大和用Kadane算法、数组重叠区间合并用排序加扫描。做题的时候不要只想"这题我见过",而是想"这题属于哪个套路"。
字符串问题还有一个独立考点:字符集与编码。题目如果说字符串只包含小写字母,那可以用数组计数;如果没说,用哈希表更稳妥。这种细节决定了代码的健壮性,也是阅卷人非常看重的地方。
3.2 动态规划:从暴力递归到状态转移
动态规划是校招笔试的分水岭。60%以上的"压轴题"最终都是DP,但DP也是最难在考场上临时想清楚的。
我的经验是,遇到求"最值""方案数""可行性"的题,优先考虑DP。想DP问题的时候,按一个固定套路推进:第一步,定义状态,dp[i]到底代表什么,这个定义必须能覆盖所有子问题;第二步,找转移方程,想清楚dp[i]怎么由前面的状态推出来;第三步,确定初始化和边界,dp[0]是多少,数组边界在哪里。
如果自底向上的递推写不清楚,就先写暴力递归,再加一个记忆化数组,改成记忆化搜索。记忆化搜索的效率往往不比循环DP差多少,而且思路更直观,在考场上是个保底手段。常见的DP模型——背包、最长上升子序列、最长公共子序列、区间DP——都要提前准备好模板,尤其是背包问题,几乎每年都有公司在笔试里考。
3.3 数据结构组合拳:栈、队列、哈希表
有些题不考复杂的算法,而是考你对数据结构的理解深度。
栈的高阶用法是单调栈,它能解决"下一个更大元素""柱状图最大矩形"这类问题。队列的高阶用法是单调队列,专门处理滑动窗口里的最值问题。哈希表则是空间换时间的核心工具,检查存在性、去重、统计频率都离不开它。堆处理TopK问题和合并有序链表特别方便,你要是不想每次手写堆,也要知道语言自带的优先队列怎么用、怎么自定义比较器。
刚入门的时候,很多人觉得用数组模拟就行,没必要特意学这些结构。但做题做到一定程度会发现,结构选对了,代码量和运行时间会差一个量级。比如一道题你用普通数组每次遍历找最小值,复杂度O(n^2),换成单调队列/单调栈,直接降到O(n)。
3.4 模拟与边界:题意理解能力的分水岭
模拟题是另一个极端,不考算法,考阅读理解能力。题目给你一个复杂的规则描述,让你按规则模拟整个过程。
这类题最容易踩的坑不是算法难,而是漏掉规则细节。比如说日历题里的闰年判断、进制转换里的负数处理、字符串题目里的空串和单字符、矩阵题目里的越界访问。
我的习惯是在动手写代码之前,先手动模拟一遍输入输出样例,确认自己理解了规则,再开始写。写的过程中,把题目里每个约束条件都对应到代码的某个判断上。如果模拟完样例发现逻辑没问题,再多想几个边界场景:输入为空时怎么办?只有一个元素时怎么办?最大值和最小值同时出现时怎么办?这些边界场景往往是判题机里的隐藏测试点,写全了才能AC。
4. 实操案例:一道“满减凑单”题从读题到AC
4.1 题目描述与第一步分析
下面这道题,是我根据当年美丽联合这类电商公司笔试的风格,重构出来的一个代表性案例,专门用来演示拿到一道在线编程题后的完整思考过程。
某电商平台有一批参与活动的商品,价格分别为
prices[0], prices[1], ..., prices[n-1],每个商品最多买一件。平台设置了满减门槛m,即订单总价不低于m元才能参加满减。现在需要你选出若干件商品,使得总价不低于m,并且在所有满足条件的方案中总价最小。如果总价最小的方案不唯一,则选择使用商品数量最少的方案。返回两个整数:最小总价和对应的商品数量。如果所有商品加起来都达不到门槛,返回-1, -1。
读完题,先别急着写代码。我习惯先把题目的数学本质剥出来:给定一个正整数数组,求一个子集,使子集和>= m,并且子集和尽可能小;在子集和相同的方案中,选元素个数最少的。这本质上是一个带约束的子集和问题,可以归类到01背包变体里。
这种业务包装题,剥壳的功夫很关键。题目里那些"满减""订单""门槛",全部不影响算法本质。真正有用的信息就是三句话:子集、最小和、最少件数。
4.2 暴力解法的推导与局限
最直观的想法是枚举所有子集,计算每个子集的和以及元素个数,然后按条件筛选。子集数量是2^n,当n小于等于20的时候,这个方案可行,代码也很简单:
def brute_force(prices, m): n = len(prices) best_sum = float('inf') best_cnt = float('inf') for mask in range(1 << n): total = 0 cnt = 0 for i in range(n): if mask & (1 << i): total += prices[i] cnt += 1 if total >= m: if total < best_sum or (total == best_sum and cnt < best_cnt): best_sum = total best_cnt = cnt if best_sum == float('inf'): return -1, -1 return best_sum, best_cnt这个解法放在小数据范围里完全没问题,但笔试平台通常会有一组数据专门卡暴力,比如n = 100。2^100这个数量级,判题机跑一百年也跑不完。所以暴力解法只能作为思路热身,用来验证你对题目的理解是否正确,最终还是要走优化的路。
4.3 优化解法:01背包思路的完整实现
既然是最小化和的问题,往动态规划上想就顺理成章了。
我定义dp[s]表示凑出总价s所需的最少商品件数。初始化dp[0] = 0,其余位置设为无穷大。每遍历一个商品价格p,就尝试用这个商品去更新所有可能的总价。这里要注意,更新必须倒序遍历总价,否则一件商品会被使用多次,那就变成完全背包了。
还有一个可以优化的点:总价上限不需要开到所有商品的总和。因为如果某个方案的总价>= m + max(prices),那从这个方案里随便去掉一个商品,总价会下降,但仍然可能>= m;就算下降到m以下,也会落在一个比原方案更小的总价上。所以最优解的总价一定不超过m + max(prices) - 1范围内,或者说我们只需要考虑m + max(prices)这个上界以内的状态。这样能省不少内存。
def min_items(prices, m): if not prices: return -1, -1 total = sum(prices) if total < m: return -1, -1 max_price = max(prices) limit = min(total, m + max_price) # 状态空间上界 INF = float('inf') dp = [INF] * (limit + 1) dp[0] = 0 for p in prices: # 倒序遍历,保证每个商品只用一次 for s in range(limit, p - 1, -1): if dp[s - p] + 1 < dp[s]: dp[s] = dp[s - p] + 1 for s in range(m, limit + 1): if dp[s] != INF: return s, dp[s] return -1, -1这段代码的时间复杂度是O(n * limit),limit不超过m + max(prices)。如果题目中m和数据规模的乘积在可接受范围内,就能通过全部测试点。空间复杂度是O(limit),只保留一维数组,避免了二维数组的内存浪费。
4.4 边界测试与在线提交
代码写出来还不算完,提交之前我会先在心里列出几个边界场景,逐一验证。
prices为空数组:直接返回-1, -1。- 所有商品总价小于
m:返回-1, -1。 m等于0:任意一个商品都满足条件,此时应该选价格最低的商品,返回最小价格和1。- 只有一个商品且价格刚好等于
m:返回m和1。 - 商品价格可能有重复:DP状态下更新时会自然合并,不需要额外处理。
- 如果题目给的商品价格是小数,要留意浮点误差。笔试中遇到价格尽量把单位转成分,用整数运算,避免精度问题。
另外,提交前记得删掉调试用的print。别笑,每年都有人因为忘了删调试输出,导致输出格式不对,被判Presentation Error甚至Wrong Answer,非常可惜。
5. 实战避坑:在线笔试里那些年踩过的坑
5.1 时间超限(TLE)的真凶与排查
我看到的线上判题表现中,TLE大概是仅次于WA的第二大错误类型。它最气人的地方在于,算法思路是对的,就是跑不过去。
常见真凶有四个。第一,复杂度选高了,数据范围该用O(n log n)你却写了O(n^2)。第二,输入输出太慢,用cin不关同步、用Scanner读十万级以上整数的时候,性能差距能到几倍。第三,循环内部做了无意义的高开销操作,比如在循环里反复拼接字符串、反复创建对象。第四,死循环,while条件写错,或者指针没有前进,这种情况最坑,代码仿佛卡住了一样。
遇到TLE,先把复杂度算一遍,对照数据范围确认算法量级没问题,再看输入输出,最后看循环里有没有高开销操作。本地生成一组最大规模的数据去测一下运行时间,是个很高效的办法。
5.2 空间超限(MLE)与全局变量陷阱
空间超限相比TLE出现得少一些,但一旦出现,往往是大数组声明不当导致的。
有些人习惯开一个很大的二维数组,比如int dp[1005][1005],如果不需要填满整个矩阵,可以用滚动数组、一维数组或者vector来按需分配。Python里则是列表套列表,稍微不注意就内存爆炸。
有些语言里还有全局变量的坑。比如C++全局数组在多组测试用例场景下,如果上一次的测试结果没有清空,下一组数据就会用到脏数据。我见过太多人在多测试用例的题里,因为忘了把全局数组重新初始化,WA到怀疑人生。处理这种场景,干脆把数组声明在函数内部,每次进入函数重新申请,虽然多了一点开销,但至少不会脏。
5.3 边界条件:空输入、单元素、极大极小的雷区
边界条件是笔试判题里最经典的暗器。样例能跑通,一到隐藏测试点就挂,十有八九是边界没处理好。
我建议在做任何一道题时,都形成条件反射,检查这几类边界:
- 输入为空:空数组、空字符串、空列表。
- 输入只有一个元素:单元素数组或单字符字符串。
- 数值最值:
int范围内的最大值、最小值,long long范围内的极端情况,比如乘法溢出。 - 零和负数:题目虽然声明了正整数,但如果是整数没有额外说明,要把0和负数情况考虑到。
- 结果很大的取模问题:不要到最后才取模,运算过程中就要取模,否则可能溢出。
边界判断写得全,不仅是为了AC,也是在告诉阅卷人:这个人对代码的健壮性有意识。
5.4 常见问题速查表
最后整理一个速查表,方便你在考场上一目了然地定位问题。
| 判题结果 | 常见原因 | 排查思路 |
|---|---|---|
| Wrong Answer (WA) | 逻辑错误、边界漏判、浮点误差 | 打日志对比中间变量,检查边界状态 |
| Time Limit Exceeded (TLE) | 复杂度过高、IO太慢、死循环 | 先算复杂度,再优化IO,最后查循环 |
| Memory Limit Exceeded (MLE) | 大数组过多、递归栈过深 | 压缩状态,用迭代代替递归 |
| Runtime Error (RE) | 数组越界、除零、栈溢出 | 检查下标,检查除数,考虑栈深度 |
| Presentation Error (PE) | 输出格式错误,多余空格或换行 | 对照题目输出格式逐字符检查 |
6. 备考与临场发挥:从笔试到Offer的经验之谈
6.1 笔试前一个月的刷题策略
如果离笔试还有一个月,我不建议每天随机刷题。效率最高的方式,是按专题刷。
可以把常见专题分成几个阶段:第一周搞定数组、字符串、链表,第二周搞定树、图、搜索,第三周主攻动态规划和贪心,第四周用来做模拟卷和复盘错题。每天不用贪多,两三道题足够了,但要保证每道题都真正吃透。
"吃透"的标准是什么?我的标准是:看完题目能主动说出它属于哪个专题、有哪些可能的解法、复杂度各是多少,然后不看题解把它完整写出来。写完之后,再看一遍有没有更优的解法。很多题目的最优解和暴力解之间,可能只差一个数据结构或者一个状态定义,但这个差距就是笔试分数拉开的地方。
模板代码值得整理。排序、二分查找、DFS、BFS、单调栈、单调队列、背包DP、并查集,这些是高频模板,整理成自己的代码片段。考试时不是让你背代码,而是让你省掉一些基础代码的编写时间,把精力留给思考。
6.2 考场上的时间分配与心态
在线笔试通常给90到120分钟,题量大概三到四道。最忌讳的是在一道题上死磕到底。
我自己的策略是:拿到卷子,先把所有题都扫一遍,在心里给每道题标个难度。然后从最有把握的题开始做,先把能拿的分拿稳,再回头啃难题。每道题卡在30分钟左右没有进展,就先跳过去,等所有会做的题都AC了再回来想。
心态上还有一个关键点:在线编程题不是竞赛,不需要追求每道题都满分,保证简单题和中档题的通过率,通常就能进入面试轮。碰上一道完全没思路的题,不要慌,把你想到的暴力解法写出来,能拿一部分测试点的分就拿一部分。笔试看的是综合表现,一道题空着是最亏的。
6.3 关于在线编程题的一些个人体会
做了这么多年技术,自己考过试,也给别人出过题、改过卷,我有一个很深的体会:在线编程题真正筛选的,不是谁见多识广,而是谁在压力下还能保持清晰的思维。
代码是要给人看的,也是要运行的。你在笔试里写的每一行代码,都会成为未来同事判断你是否专业的一个样本。命名规范一点,逻辑清楚一点,边界想全一点,这些习惯不仅让你多拿分数,也会在之后的工作里持续受益。
最后再分享一个小技巧:考前一定要去目标公司的笔试平台上做一次模拟,哪怕只是熟悉一下代码编辑器的位置、输入输出的方式、提交按钮在哪里,都能显著降低考场的陌生感。很多翻车不是输在实力上,而是输在第一次用不熟悉的平台。把这些细节都准备好,把该踩的坑提前踩一遍,真正的笔试就会变得从容很多。