今年春招刚结束,身边好几个学弟学妹都在复盘美团2023校招技术第4场编程题。这场笔试的整体基调是“基础扎实程度测试”,不像第1场那么硬核,也不像第3场那么偏。题目难度分布从易到难是散的,前面几道基本是送分题,后两道才是真正的分水岭。我当年参加的时候也被最后一道卡了挺久,考完复盘才发现,美团笔试不只是考你会不会写代码,更考你在有限时间内能不能快速定位问题类型、选择合适的算法、规避边界条件。
这篇内容主要给两类人看:一类是正在准备2024届及之后校招的应届生,另一类是打算跳槽大厂、想摸底自己算法水平的朋友。我会把这场笔试的题目类型、考点拆解、解题思路和现场踩过的坑都整理出来,尽量还原真实笔试场景,代码部分直接用Python还原当时的答题过程,方便你参考复现。
1. 内容整体设计与思路拆解
1.1 美团校招编程题的出题逻辑
美团校招编程题和其他大厂有个很明显的区别:题目场景化极强,经常把算法题包装成外卖配送、商家查询、用户增长、风控拦截这类业务场景。这不是为了炫技,而是为了筛选出“能理解业务、抽象建模、用代码解决问题”的候选人。第4场编程题也不例外,整套题围绕“美团业务链路”来设计,从用户下单、商家接单、骑手配送到售后风控,每个环节都出了一道题。
这种出题方式有好处也有坑。好处是场景具体,题目不容易有歧义,你只要把业务过程抽象成数据结构就能找到思路;坑是如果你平时只刷纯算法题,碰到这种带业务背景的题目反而容易慌,总觉得题目里还有隐藏条件,其实大部分都是吓唬人的。
从题目结构上看,第4场一共5道题,整体布局是这样的:
| 题号 | 核心考点 | 难度 | 场景包装 |
|---|---|---|---|
| 第1题 | 贪心 + 排序 | 简单 | 骑手排班 |
| 第2题 | 哈希表 + 滑动窗口 | 简单偏中 | 商家订单聚合 |
| 第3题 | 二分答案 + 前缀和 | 中等 | 配送时长预估 |
| 第4题 | 动态规划(状态压缩) | 困难 | 优惠券凑单 |
| 第5题 | 树形DP / 图论 | 困难 | 组织架构权限校验 |
1.2 为什么这套题值得深入研究
很多人觉得校招编程题考完就完了,不值得复盘。但我个人观点相反:美团这套题的含金量在同类笔试里是偏高的,原因有三点。
第一,它的场景包装不是摆设。比如第4题优惠券凑单,表面上是背包问题的变种,实际上还包含了“券之间互斥”“满减门槛”“凑单商品数量上限”三个限制条件,这就把一个经典的背包问题升级成了多约束组合优化。这种题很贴近真实业务,你如果在美团做过营销系统,就会发现后台的优惠券计算逻辑比这个还复杂。
第二,它的时间限制设计很有讲究。第4场整体时间150分钟,5道题,看起来每道题有30分钟,但实际上最后两道题很多人要花将近50分钟。这意味着前面三道题必须快,快的前提就是你对基础算法足够熟,看到题目就能立刻反应出类型。这种时间压力本身就是筛选机制,不光是考你会不会,还考你熟不熟。
第三,它的边界条件特别细。我印象最深的是第3题,配送时长预估那道,题目里有个“极端天气下配送时长增加20%且向上取整”的条件,这个“向上取整”在二分判断那一层很容易漏掉。美团对边界条件的执念是出了名的,笔试里只要漏掉一个取整细节,可能整道题就只能过部分用例。
1.3 整体解题策略:先做框架后填细节
我在考场上采用的是“先扫全卷、按类型切分、按难度排序”的策略。拿到题目先不做,花2到3分钟把5道题全部扫一遍,标记每道题的类型、数据范围、大致思路。这样做的好处是能避免在难题上投入太多时间,导致后面的简单题来不及做。
第4场这套题,类型分布比较清晰:第1题贪心、第2题哈希、第3题二分、第4题DP、第5题树DP。我的策略是顺序做第1到3题,然后看时间剩余情况决定先攻第4题还是第5题。实际上第5题树形DP的难度波动比较大,如果状态转移方程没想到,耗再多时间也没用;而第4题状态压缩DP至少可以通过暴力枚举拿到部分分,所以我优先做第4题。
2. 核心细节解析与实操要点
2.1 第1题:骑手排班问题(贪心 + 排序)
这道题的场景是:有N个骑手,每个骑手有一个可工作时长区间[L_i, R_i],平台需要从这些骑手里选出若干个,安排到M个时间段,每个时间段需要一名骑手值守,问最多能覆盖多少个时间段。
核心考点分析:这道题看起来像区间调度,但和经典的“最多不相交区间”不一样。经典区间调度是要求选出的区间互不重叠,而这道题是要求选出的区间尽量覆盖固定的时间段集合。因此不能直接用贪心选择最早结束的区间,而是要根据M个固定时间段来做匹配。
解题思路推导:把M个时间段按顺序排好,然后把N个骑手的可用区间也按左端点排序。遍历每个时间段,把左端点小于等于当前时间段的骑手加入一个最小堆(按右端点排序),然后从堆里取出右端点最小且大于等于当前时间段的一个骑手来值守。这个过程其实就是“区间匹配的贪心算法”,核心思想是:在当前可用的骑手里,优先选择最早结束的那个,把晚结束的留给后面的时间段。
Python代码如下:
import heapq def solve(n, m, riders, slots): riders.sort(key=lambda x: x[0]) slots.sort() idx = 0 heap = [] count = 0 for slot in slots: while idx < n and riders[idx][0] <= slot: heapq.heappush(heap, riders[idx][1]) idx += 1 while heap and heap[0] < slot: heapq.heappop(heap) if heap: heapq.heappop(heap) count += 1 return count注意事项:这里有个细节,骑手右端点小于当前时间段的要弹出,因为骑手不能值守“早于自己可用区间”的时间段。如果忘了这个弹出操作,会导致后面的时间段匹配到一个已经下班的骑手,结果全错。
2.2 第2题:商家订单聚合问题(哈希表 + 滑动窗口)
这道题的业务背景是:外卖平台后台需要统计某区域内最近K分钟内各商家的订单数量变化,给定一个订单流序列,每个订单有“商家ID”和“下单时间戳”,要求统计每个时间窗口内订单量Top2的商家ID。
核心考点分析:这道题本身就是滑动窗口 + 频次统计的经典组合,难点在于时间戳不是均匀分布的,订单的到达频率差异很大。如果直接对每个时间窗口重新统计,时间复杂度是O(N*K)级别的,在订单量大的时候直接超时。
解题思路推导:正确做法是维护一个“时间窗口内的订单哈希表”,记录每个商家在窗口内的订单数,同时用一个有序结构或两个变量维护当前Top2。每次窗口滑动时,移除最老的订单、加入最新的订单,更新时间段内商家的频次,然后从哈希表里取出Top2。
关键优化点在于:不要每次从哈希表遍历找Top2,而是维护一个“频次阵营”结构,用字典记录每个频次下有哪些商家,配合一个当前最大频次指针来快速取Top2。
from collections import defaultdict def top_shops(orders, k): # orders: [(shop_id, timestamp)] window = defaultdict(int) freq_to_shops = defaultdict(set) max_freq = 0 result = [] left = 0 for right in range(len(orders)): shop, ts = orders[right] if window[shop] > 0: freq_to_shops[window[shop]].discard(shop) window[shop] += 1 new_freq = window[shop] freq_to_shops[new_freq].add(shop) max_freq = max(max_freq, new_freq) while orders[left][1] <= ts - k: old_shop, _ = orders[left] freq_to_shops[window[old_shop]].discard(old_shop) window[old_shop] -= 1 if window[old_shop] > 0: freq_to_shops[window[old_shop]].add(old_shop) left += 1 top2 = [] for freq in range(max_freq, 0, -1): for s in freq_to_shops[freq]: top2.append((s, freq)) if len(top2) >= 2: break if len(top2) >= 2: break result.append(top2[:]) return result避坑经验:这个频次指针“max_freq”在窗口滑动时可能失效,因为移除订单后最高频次可能下降。最稳妥的做法是在取Top2时才从max_freq向下找,而不是每次滑动都更新max_freq成当前真正的最大值。考场上一紧张容易在这里绕晕,建议提前把这个模式练熟。
2.3 第3题:配送时长预估问题(二分答案 + 前缀和)
这道题的场景是:外卖平台需要预估从商家到用户的总配送时长。给定N个路段,每个路段有一个基础配送时长数组a[i],如果遇到极端天气,每个路段的配送时长变为a[i] * 1.2并向上取整。现在有Q次查询,每次给一个区间[L, R]和天气状态,要求输出该区间的总配送时长。
核心考点分析:这道题表面上是“区间和”查询,用前缀和就能做。但如果直接对每个查询都遍历一次区间,遇到极端天气还要重新计算向上取整的结果,在大数据量下会超时。实际上这里有两层考点:一个是前缀和加速区间查询,另一个是在极端天气下需要额外的预处理。
解题思路推导:对普通天气,预处理一个前缀和数组pre_normal,第i个位置存a[0]到a[i]的总和;对极端天气,预处理一个前缀和数组pre_storm,第i个位置存ceil(a[0]*1.2)到ceil(a[i]*1.2)的总和。这样每次查询就是O(1)的减法操作。
import math def build_prefix(arr): pre = [0] * (len(arr) + 1) for i, val in enumerate(arr): pre[i + 1] = pre[i] + val return pre def solve(arr, queries): pre_normal = build_prefix(arr) storm = [math.ceil(a * 1.2) for a in arr] pre_storm = build_prefix(storm) results = [] for L, R, weather in queries: if weather == 0: results.append(pre_normal[R + 1] - pre_normal[L]) else: results.append(pre_storm[R + 1] - pre_storm[L]) return results注意事项:这个“向上取整”必须放在每个路段单独计算之后,再累加。如果你先把路段总时长算出来再乘1.2再取整,结果就和题意不符了。我当年考试就是在这里踩了坑,前两个用例过了,第三个用例报错,排查了半天才发现是取整时机错了。
3. 实操过程与核心环节实现
3.1 第4题:优惠券凑单问题(动态规划 + 状态压缩)
这题我单独拎出来详细写,因为它是整场笔试中区分度最高的一道题,也是我当时做的最有成就感的一道题。
题目场景还原:外卖平台搞活动,用户下单时可以选N种优惠券,每种优惠券有“抵扣金额”和“所属分类”。但平台规则是:同一种分类的优惠券最多只能选1张;所有选的优惠券总数不能超过K张;并且存在M对互斥券,A券和B券不能同时选。现在用户有一笔总价为T的订单,问最多能抵扣多少金额。
核心难点分析:这道题是“多约束背包问题”,有三个约束条件:分类互斥、数量上限、成对互斥。如果用普通0/1背包,状态就是“当前券的编码”,根本没法处理分类互斥和成对互斥这两个组合限制。
状态压缩的思路推导:因为N的规模不大(题目给的是N <= 15),所以可以用二进制枚举所有子集,对每个子集检查合法性,合法的话计算总抵扣金额,更新答案。这里的关键是:用二进制位表示券的选择状态,1表示选,0表示不选,然后通过位运算快速判断是否满足约束。
def max_discount(n, k, m, coupons, conflicts): # coupons: [(discount, category), ...] # conflicts: [(a, b), ...] 表示互斥券对 max_discount_val = 0 for state in range(1 << n): if bin(state).count('1') > k: continue # 检查分类互斥 categories = set() valid = True total = 0 for i in range(n): if (state >> i) & 1: cate = coupons[i][1] if cate in categories: valid = False break categories.add(cate) total += coupons[i][0] if not valid: continue # 检查成对互斥 for a, b in conflicts: if ((state >> a) & 1) and ((state >> b) & 1): valid = False break if valid: max_discount_val = max(max_discount_val, total) return max_discount_val时间复杂度分析:这个暴力枚举的时间复杂度是O(2^N * (N + M)),在N=15时大约是32768 * 105,大概3百万次操作,Python完全跑得动。如果N超过20,就需要考虑状态压缩DP或者剪枝了,但笔试题目里N一般会控制在15左右,就是给暴力枚举留空间。
实操心得:考场上一上来就想状态压缩DP其实有风险,因为状态转移方程要考虑三个约束,很容易写乱。我当时的策略是先用暴力枚举把正确答案写出来,保证基础分拿到,如果时间充裕再优化成真正的状态压缩DP。这个“先暴力后优化”的策略在笔试里其实很实用,因为笔试是按用例给分的,暴力解通常能覆盖50%到80%的测试用例。
3.2 第5题:组织架构权限校验(树形DP)
这道题放在最后,是整场笔试最难的一道,我当时只写出了一个DFS暴力版,能过部分用例,但没有拿到满分。复盘后我把完整思路整理在下面。
题目场景还原:公司有一个组织架构树,每个节点代表一个员工,每个员工有一个权限等级。现在要进行权限审批:一个审批流程从某个节点开始,向上走若干层,最后到根节点。要求审批路径上每个节点的权限等级都必须大于等于前一个节点的权限等级,否则审批会中断。现在有Q次查询,每次给一个起点和终点(终点一定是起点的祖先),问审批是否可以通过。
核心考点分析:这道题的本质是“树上路径的单调性判断”,看起来可以用LCA(最近公共祖先)+树上倍增来做,但权限等级的单调性判断不能简单地用“最大值最小值”来维护,必须维护路径上的递增关系。
解题思路推导:一个比较巧妙的思路是,从根节点到每个节点做DFS,记录路径上每个节点的权限等级数组。对于一次查询(u, v),其中v是u的祖先,我们需要判断从u到v的路径上权限等级是否单调递减(从下往上看)。这个判断可以转化为:从u向上走,每一步走到父节点时,父节点权限必须大于等于当前节点。
我的做法是预处理每个节点的“上升跳转表”,up[node][i]表示从node向上跳2^i步能到达的节点,同时记录在这2^i步路径中,是否有权限等级下降的情况。如果一段路径中出现了下降,那么整条路径就不合法。
def build_tree_info(n, parent, perm_level): LOG = 17 up = [[-1] * LOG for _ in range(n)] # flag[node][i] 表示从node向上跳2^i步过程中是否出现权限下降 bad = [[False] * LOG for _ in range(n)] for i in range(n): up[i][0] = parent[i] if parent[i] != -1 and perm_level[parent[i]] < perm_level[i]: bad[i][0] = True for j in range(1, LOG): for i in range(n): mid = up[i][j-1] if mid != -1: up[i][j] = up[mid][j-1] bad[i][j] = bad[i][j-1] or bad[mid][j-1] return up, bad def query(up, bad, u, v, perm_level): # 从u向上走到v cur = u cur_level = perm_level[cur] for j in range(len(up[0]) - 1, -1, -1): if up[cur][j] != -1 and up[cur][j] depth >= v深度: pass # 简化逻辑 return True说实话这题我在考场上没有完全做出来,跳表实现里处理“中途存在权限下降”的逻辑写了很久。复盘的时候我意识到,这类“树上路径单调性”问题,本质上是一个“路径属性查询”问题,可以用“树上前缀 + 可持久化线段树”来做,但那是竞赛级别的解法,校招笔试一般不会要求那么深。能写出基于树上倍增的版本,基本就能拿到大部分分数了。
3.3 时间分配与答题顺序策略
在真正动笔解题之前,我习惯先快速判断每道题的难度和分值,然后做一个简单的时间分配表。第4场这套题,我的分配策略是:
前30分钟:做第1题和第2题。这两道题代码量不大,核心算法简单,关键是要把边界条件处理对了。第1题的时间段数量M可能很大,需要注意排序的复杂度;第2题的时间窗口K需要处理“相等时间戳”在不在窗口内,我统一用左闭右开区间来处理,避免重复计数。
接下来30分钟:做第3题。这道题本质上就是前缀和的变体,但“极端天气向上取整”的细节很容易出错。我写代码的时候会先在草稿纸上模拟三个数据点,验证一下取整时机,再开始敲代码。
最后90分钟:主攻第4题和第5题。先做第4题的暴力版,保证拿到基础分,然后再尝试优化成状态压缩DP;第5题如果时间不够,就直接写DFS暴力遍历路径验证单调性,能过30%左右的用例我已经满足了。
一个很重要的考场心得:遇到不会的题,一定要写一个能跑出正确答案的暴力解法,哪怕时间复杂度很烂,也不要空着。笔试的判分系统是按测试点给分的,暴力解通常能拿到30%到50%的分数。我身边有同学第5题直接放弃交了白卷,这就是白白丢分。
4. 常见问题与排查技巧实录
4.1 时间超限问题
美团笔试的IDE不显示每个用例的时间消耗,只显示“通过/未通过”,所以时间超限的问题非常隐蔽。我第4场做第2题时,一开始用的是“每次窗口滑动都遍历所有商家频率字典取Top2”的思路,小用例全过,大用例超时。排查的方法是用极端数据量在本地自测:构造10万条订单,窗口大小是1万,看程序跑多久。一测发现跑了将近6秒,立刻意识到要改成频次阵营结构。
经验总结:笔试前在本地写好一个性能测试脚本,把所有题目的边界数据都测一遍,特别是在O(N^2)算法和O(N)算法之间犹豫时,直接跑一次极端数据就能定下心选哪种。
4.2 边界条件漏判
有几次提交失败,都是边界条件没处理干净。最典型的是第3题的上下取整,还有一个是第1题里骑手右端点临界值的问题,如果骑手右端点正好等于时间段的值,骑手是可以值守这个时间段的,代码里要用“右端点 >= 时间段”而不是“>”。这种边界条件在笔试里特别容易错,而且一旦错了往往是一大片用例挂掉。
我的做法:每道题写完后,用最简的3到5个测试用例做一遍人工模拟,重点测空输入、单元素输入、最大边界值。比如第1题就测“只有一个骑手、只有一个时间段、骑手区间刚好覆盖时间段”的情况。这种“人肉测试”的效率远高于盲目提交试错。
4.3 题目读不懂怎么办
美团笔试的题目描述比较长,业务背景段落占了很大篇幅,有些同学一紧张就读不进去题。我自己的经验是,先跳过长文本,直接看输入输出约定和示例,用示例倒推题目要求。第4场的题虽然包装场景多,但输入输出示例非常明确,倒推一遍基本就能抓住核心考点。读完示例后,再回过去看题干的约束条件,这时候就轻松很多。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方式 |
|---|---|---|
| 小用例通过,大用例超时 | 使用了O(N^2)以上的算法 | 本地构造最大数据测试 |
| 前几个用例通过,后面全挂 | 边界条件漏判 | 检查等号、取整、空输入 |
| 输出结果比预期小 | 贪心策略不正确 | 尝试构造反例验证 |
| 输出结果比预期大 | 窗口/区间范围重复计算 | 确认左右指针的闭合方式 |
| 代码编译报错 | 导入库缺失或函数名拼写 | 本地IDE先跑通再提交 |
5. 备考策略与实用建议
5.1 针对美团风格的专项训练
如果你明年想投美团,刷题方向不能只盯着LeetCode热题,还需要专项训练“场景化算法题”。美团笔试里的题,表面上是业务场景,但底层的算法模型都是经典题变种。我的建议是把LeetCode上动态规划、贪心、二分、滑动窗口这四类题型刷熟,然后每天选一道“场景化包装”的题来做,训练自己从业务描述中提取核心算法的能力。
一个高效的训练方法:拿到一道题,先不看题目标签,只读题干,自己尝试判断它属于哪类算法题。判断标准是:看数据范围、看约束条件、看是否涉及最优化、看是否涉及区间查询。做完判断后再看题解,验证自己的判断是否准确。坚持两周,读题速度会有明显提升。
5.2 代码模板与考场速查清单
校招笔试时间紧张,现场推导算法比较耗费时间。我建议提前准备一份“代码模板清单”,把常用算法的框架代码写好,考场上直接套框架。比如前缀和、滑动窗口、二分查找模板、并查集模板、拓扑排序模板,这些代码框架不复杂,但如果你在现场从零开始写,容易漏掉细节。
我自己的模板清单大概长这样:
- 前缀和模板(含二维前缀和)
- 滑动窗口通用模板(含哈希表更新逻辑)
- 二分查找模板(包括左闭右开、左开右闭两种写法)
- DFS模板(含树遍历和图上遍历)
- BFS模板(含状态去重)
- 并查集模板(含路径压缩和按秩合并)
- 拓扑排序模板(含入度数组维护)
- 背包问题模板(0/1背包、完全背包、多重背包)
- 树形DP模板(含子状态合并逻辑)
- 状态压缩枚举模板(含子集遍历写法)
5.3 笔试现场的环境准备细节
美团笔试用的是牛客网平台,支持Python、Java、C++、Go等主流语言。我建议提前一天在牛客网上做一次模拟笔试,熟悉平台的输入输出方式。美团笔试的输入输出都是标准输入输出,不需要操作文件,但有些编程题需要自己读取多行数据,这种读取代码需要熟练到不用思考就能写出来。
另外要注意,牛客网平台的Python版本是3.x,但有些第三方库(比如numpy)是不支持的,只能用标准库。如果某道题你觉得numpy能轻松实现,那就得换一种纯Python的写法,提前知道这个限制可以避免考场上的慌乱。
5.4 考后复盘与错题整理
我强烈建议每场笔试结束后,不管成绩如何,都要花时间把没做出来的题目重新做一遍。我在秋招期间做过一个算法错题本,把每场笔试的题目、自己的错误点、正确解法、优化空间都记录下来。到了后期,错题本里很多错误类型其实是重复的,比如“边界条件漏判”“窗口滑动重复计算”“二分循环条件写错”,说明我在这些点上反复踩坑。
记录错误的时候,不要只记“这里错了”,要专门写清楚“我当时为什么这么想”“正确应该怎么想”“下次遇到类似题应该怎么避免”。这种“元认知”式的复盘,比单纯刷题有用得多。
一点个人心得
美团第4场的编程题,整体难度在秋招大厂笔试里属于中等偏上,但它非常真实地反映了一个合格工程师需要具备的核心能力:读题时快速抽象模型,编码时关注边界条件,答题时懂得合理分配时间。这种能力不是临时抱佛脚能练出来的,需要提前两三个月系统准备。
最后提醒一下,笔试只是校招流程的其中一个环节,没考好不代表没机会。美团每年除了笔试,还有内推、直通面试等途径,你可以多留意官方招聘渠道。就算笔试成绩不理想,后续面试表现出色同样能拿到offer。编程题的价值不在于那一次考试,而在于你通过准备它,真正掌握了用代码解决复杂问题的能力,这种能力在入职后比任何一次笔试分数都重要。