大厂真题 / 美团
美团 5.9 笔试真题 - 算法岗
本场考试概述
考试时间:2026年5月9日 考试岗位:算法岗 难度评级:困难
考点分析:
- 第一题:线性扫描(难度简单)
- 第二题:ML/DL 实现 · 标准化 + Adam + Warm-up + 早停(难度中等)
- 第三题:二分答案 + 删点插点验证(难度困难)
- 第四题:DP + 线段树(难度困难)
建议策略:
- 第一题作为送分题必拿,思路简单一遍过
- 第二题考的是 NumPy 手写训练流程,参数全部锁死,比拼工程功底,能否完整实现是关键
- 第三、四题难度较大,建议先拿部分分即可,再冲正解
第一题:平稳风段
题目描述
给定长度为 $n$ 的风速序列和阈值 $d$。定义连续段 $[l, r]$ 为”平稳段”,当且仅当对所有相邻位置都有 $\lvert a_i - a_{i-1} \rvert \leq d$。请输出所有平稳段的最大长度。
多组测试数据,保证所有测试数据的 $n$ 之和不超过 $10^5$。
样例
输入
2
5 2
3 4 7 6 7
1 0
100
输出
3
1
思路分析
序列上找最长连续段,使得段内每对相邻元素差不超过 $d$。判定条件仅涉及相邻两个元素,一次线性扫描即可。
维护 cur 表示以当前位置为右端点的平稳段长度。如果 $\lvert a_i - a_{i-1} \rvert \leq d$ 则 cur += 1;否则段已断开,cur 重置为 1。每步用 cur 刷新全局最长 best。
题解代码
import sys
input = sys.stdin.readline
def solve():
T = int(input())
out = []
for _ in range(T):
n, d = map(int, input().split())
a = list(map(int, input().split()))
cur = 1
best = 1
for i in range(1, n):
if abs(a[i] - a[i - 1]) <= d:
cur += 1
if cur > best:
best = cur
else:
cur = 1
out.append(str(best))
print("\n".join(out))
solve()
复杂度分析
时间复杂度:$O(n)$ 空间复杂度:$O(n)$
第二题:带调度的逻辑回归
题目描述
实现一个仅使用 NumPy 的二分类训练流程,包括预处理、Adam 优化器、学习率预热与早停。
具体要求:
- 数据预处理:对训练集逐列计算均值 $\mu$ 与总体标准差 $\sigma$(使用
ddof=0);将训练、验证、测试集按 $(x - \mu) / \sigma$ 标准化;若某列 $\sigma = 0$ 则置为 1 - 模型:$p = \text{sigmoid}(w \cdot x + b)$,损失为带 L2 正则的二元交叉熵,正则系数 $\lambda = 10^{-4}$
- Adam 优化器:$\beta_1 = 0.9$,$\beta_2 = 0.999$,$\varepsilon = 10^{-8}$,初始学习率 $\text{lr} = 0.01$,批量大小 16
- Warm-up:前 5 个 epoch 学习率从 0 线性增长到 $\text{base_lr}$;第 6 个 epoch 起固定
- 早停:最大训练 100 个 epoch,监控验证集损失;连续 10 个 epoch 没有出现 $10^{-6}$ 的改进则停止。训练结束后还原为最优权重
- 推理:对测试集计算概率,阈值 0.5 输出标签 0/1
- 参数 $w, b$ 全部初始化为 0;使用
np.random.seed(42)进行 shuffle
输入:单行 JSON 对象,结构为 {"train": [...], "val": [...], "test": [...]}。train/val 中每个样本末尾一个元素为标签。
样例
输入
{"train":[[-3,-3,0],[-2.5,-2.5,0],[-3.1,-2.9,0],[3,3,1],[2.7,3.2,1],[3.3,2.8,1]],"val":[[-2.8,-2.9,0],[3.1,2.9,1]],"test":[[-3,-3],[3,3]]}
输出
[0, 1]
思路分析
第一步:标准化
用训练集逐列均值 $\mu$ 与总体标准差 $\sigma$(ddof=0)变换三个数据集。$\sigma = 0$ 时强制设为 1 避免除零。标准化保证各维度量纲一致,有利于优化器收敛。
第二步:模型与损失
逻辑回归模型:$z = w \cdot x + b$,$p = \text{sigmoid}(z)$。损失为带 L2 正则的 BCE:
\[\mathcal{L} = -\frac{1}{N}\sum[y \log p + (1-y)\log(1-p)] + \frac{\lambda}{2}\|w\|^2\]梯度:$\nabla_w = \frac{1}{B} X^T(p - y) + \lambda w$,$\nabla_b = \frac{1}{B}\sum(p - y)$。
第三步:Adam 优化器
Adam 维护一阶动量 $m$(梯度的指数移动平均)和二阶动量 $v$(梯度平方的指数移动平均)。由于 $m, v$ 初始为 0,前几步估计被低估,需要偏置修正:$\hat{m} = m/(1 - \beta_1^t)$,$\hat{v} = v/(1 - \beta_2^t)$。参数更新:$\theta \leftarrow \theta - \text{lr} \cdot \hat{m} / (\sqrt{\hat{v}} + \varepsilon)$。
第四步:Warm-up
前 5 个 epoch 学习率从 0 线性升到 base_lr。这给 Adam 的动量积累一段缓冲,避免初期步长过大造成震荡。
第五步:早停
每 epoch 末算一次验证损失。改进必须严格超过 $10^{-6}$ 才算有效。连续 10 个 epoch 无改进则提前停止,还原最优权重做推理。
题解代码
import json
import numpy as np
data = json.loads(input())
train = np.array(data["train"], dtype=np.float64)
val = np.array(data["val"], dtype=np.float64)
test = np.array(data["test"], dtype=np.float64)
X_train, y_train = train[:, :-1], train[:, -1]
X_val, y_val = val[:, :-1], val[:, -1]
X_test = test
mu = X_train.mean(axis=0)
sigma = X_train.std(axis=0, ddof=0)
sigma = np.where(sigma == 0, 1.0, sigma)
X_train = (X_train - mu) / sigma
X_val = (X_val - mu) / sigma
X_test = (X_test - mu) / sigma
N, d = X_train.shape
w = np.zeros(d)
b = 0.0
m_w = np.zeros(d); v_w = np.zeros(d)
m_b = 0.0; v_b = 0.0
beta1, beta2, eps_adam = 0.9, 0.999, 1e-8
lam = 1e-4
base_lr = 0.01
bs = 16
eps_log = 1e-15
np.random.seed(42)
best_loss = float('inf')
best_w = w.copy()
best_b = b
no_improve = 0
t_step = 0
for epoch in range(1, 101):
lr = base_lr * epoch / 5.0 if epoch <= 5 else base_lr
perm = np.random.permutation(N)
Xs = X_train[perm]
ys = y_train[perm]
for i in range(0, N, bs):
xb = Xs[i:i + bs]
yb = ys[i:i + bs]
nb = xb.shape[0]
z = xb @ w + b
p = 1.0 / (1.0 + np.exp(-z))
grad_w = xb.T @ (p - yb) / nb + lam * w
grad_b = np.mean(p - yb)
t_step += 1
m_w = beta1 * m_w + (1 - beta1) * grad_w
v_w = beta2 * v_w + (1 - beta2) * (grad_w * grad_w)
m_b = beta1 * m_b + (1 - beta1) * grad_b
v_b = beta2 * v_b + (1 - beta2) * (grad_b * grad_b)
bc1 = 1.0 - beta1 ** t_step
bc2 = 1.0 - beta2 ** t_step
w -= lr * (m_w / bc1) / (np.sqrt(v_w / bc2) + eps_adam)
b -= lr * (m_b / bc1) / (np.sqrt(v_b / bc2) + eps_adam)
z_v = X_val @ w + b
p_v = 1.0 / (1.0 + np.exp(-z_v))
p_v = np.clip(p_v, eps_log, 1.0 - eps_log)
cur_loss = -np.mean(y_val * np.log(p_v) + (1.0 - y_val) * np.log(1.0 - p_v)) \
+ 0.5 * lam * np.dot(w, w)
if cur_loss < best_loss - 1e-6:
best_loss = cur_loss
best_w = w.copy()
best_b = b
no_improve = 0
else:
no_improve += 1
if no_improve >= 10:
break
w, b = best_w, best_b
z = X_test @ w + b
p = 1.0 / (1.0 + np.exp(-z))
labels = (p >= 0.5).astype(int)
print(json.dumps(labels.tolist()))
复杂度分析
时间复杂度:$O(\text{epochs} \times N \times d)$,每个 mini-batch 是 $O(B \times d)$ 的矩阵乘。 空间复杂度:$O(N \times d)$,主要是数据矩阵。
第三题:笨蛋同学
题目描述
有一个长度为 $n$ 的整数序列 $a$,每个元素满足 $1 \leq a_i \leq m$。可以执行至多一次”变换”操作:选定下标 $k$,将 $a_k$ 改为 $[1, m]$ 内任意整数。
变换后将序列升序排序得到 $b$。定义”最小间隔”为 $\min_{i}(b_{i+1} - b_i)$。
请通过至多一次变换操作,最大化这一最小间隔,并输出能达到的最大值。
样例
输入
3
3 10
1 2 8
4 7
1 1 1 7
2 100
30 70
输出
3
0
70
思路分析
第一步:观察单调性
排序后最小相邻差由相邻元素决定。至多一次”修改一个值”等价于”删一个元素 + 插一个新值 $v \in [1, m]$”。答案具有单调性:若最小间隔 $G$ 可行,则任何更小的 $G$ 也可行。因此采用二分答案。
第二步:check 函数设计
给定 $G$,先排序,扫一遍找出所有 bad(相邻差 $< G$)的位置。按 bad 数量分情况:
- 0 个 bad:零次操作就够,直接可行
- 超过 2 个 bad,或 2 个 bad 不相邻:一次操作最多消两处紧邻的违例,不可行
- 1 个 bad(位于 $a[i]$ 与 $a[i+1]$ 之间):候选删除点为 $i$ 或 $i+1$
- 2 个 bad 且紧邻(共享中间元素 $a[i+1]$):唯一候选删除 $i+1$
第三步:验证候选删除点
对每个候选 $k$,需要验证两件事:
- 删后剩余间距合规:新产生的相邻对 $(a[k-1], a[k+1])$ 的间距 $\geq G$
- 能否插入新值:在 $[1, m]$ 内存在 $v$,使其与剩余每个相邻元素的距离都 $\geq G$。三种放法——最左($v \leq \text{first} - G$)、最右($v \geq \text{last} + G$)、或塞入某个宽度 $\geq 2G$ 的内部空隙
第四步:二分框架
答案范围 $[0, m]$。上取整 mid 避免死循环,check 为真则扩大下界,否则缩小上界。
题解代码
import sys
input = sys.stdin.readline
def feasible(a, n, m, G):
if G == 0:
return True
bad = []
for i in range(n - 1):
if a[i + 1] - a[i] < G:
bad.append(i)
if len(bad) > 2:
return False
if not bad:
return True
if len(bad) == 1:
cands = (bad[0], bad[0] + 1)
elif len(bad) == 2 and bad[1] == bad[0] + 1:
cands = (bad[0] + 1,)
else:
return False
for k in cands:
if 0 < k < n - 1 and a[k + 1] - a[k - 1] < G:
continue
if any(i != k - 1 and i != k for i in bad):
continue
first = a[1] if k == 0 else a[0]
last = a[n - 2] if k == n - 1 else a[n - 1]
if first - G >= 1:
return True
if last + G <= m:
return True
if 0 < k < n - 1 and a[k + 1] - a[k - 1] >= 2 * G:
return True
for i in range(n - 1):
if i == k - 1 or i == k:
continue
if a[i + 1] - a[i] >= 2 * G:
return True
return False
def solve():
T = int(input())
out = []
for _ in range(T):
n, m = map(int, input().split())
a = sorted(map(int, input().split()))
lo, hi = 0, m
while lo < hi:
mid = (lo + hi + 1) // 2
if feasible(a, n, m, mid):
lo = mid
else:
hi = mid - 1
out.append(str(lo))
print("\n".join(out))
solve()
复杂度分析
时间复杂度:$O(n \log m)$,二分外层 $O(\log m)$,每次 check 线性扫数组 $O(n)$。 空间复杂度:$O(n)$。
第四题:弹性分桶
题目描述
给定 $n$ 个非负整数,可以重排再划分为若干”桶”。设某桶含 $k$ 个数,最大值 $\text{max}$、最小值 $\text{min}$,则该桶必须满足:$k \leq L$ 且 $\text{max} - \text{min} \leq D + E \cdot (k-1)$。
目标:使桶的总数最少。
样例
输入
2
7 3 1 3
1 2 3 7 8 10 10
7 0 0 5
5 5 5 6 6 6 6
输出
3
2
思路分析
第一步:排序后连续段最优
可以任意重排,所以最优桶一定是排序后的连续段。设桶覆盖 $a[i..j]$,约束为 $j - i + 1 \leq L$ 且 $a[j] - a[i] \leq D + E \cdot (j - i)$。
第二步:DP 定义
$f[k]$ 表示前 $k$ 个元素分桶的最少桶数。转移:$f[j+1] = \min(f[i] + 1)$,其中 $i$ 满足长度约束和值域约束。
第三步:关键变换解耦双下标
约束 $a[j] - a[i] \leq D + E(j-i)$ 移项得 $a[j] - Ej \leq a[i] - Ei + D$。令 $b[k] = a[k] - E \cdot k$,约束化为 $b[i] \geq b[j] - D$。
第四步:线段树优化
把位置按 $b$ 值升序映射到线段树叶子。进窗时激活叶子写入 $f[i]$,出窗时置回 $\infty$。查询时二分找第一个 $b \geq b[j] - D$ 的叶子位置,从那往后做区间 min。
题解代码
import sys
from bisect import bisect_left
input = sys.stdin.readline
INF = 10 ** 18
def solve():
n, D, E, L = map(int, input().split())
a = list(map(int, input().split()))
a.sort()
b = [a[i] - E * i for i in range(n)]
order = sorted(range(n), key=lambda i: (b[i], i))
pos_to_rank = [0] * n
for r, i in enumerate(order):
pos_to_rank[i] = r
sorted_b = [b[i] for i in order]
size = 1
while size < max(n, 1):
size *= 2
seg = [INF] * (2 * size)
def update(rk, val):
rk += size
seg[rk] = val
rk //= 2
while rk:
nv = min(seg[2 * rk], seg[2 * rk + 1])
if seg[rk] == nv:
break
seg[rk] = nv
rk //= 2
def query(lo, hi):
res = INF
lo += size
hi += size + 1
while lo < hi:
if lo & 1:
if seg[lo] < res:
res = seg[lo]
lo += 1
if hi & 1:
hi -= 1
if seg[hi] < res:
res = seg[hi]
lo >>= 1
hi >>= 1
return res
f = [INF] * (n + 1)
f[0] = 0
for j in range(n):
if j - L >= 0:
update(pos_to_rank[j - L], INF)
update(pos_to_rank[j], f[j])
thr = b[j] - D
lo_idx = bisect_left(sorted_b, thr)
if lo_idx < n:
res = query(lo_idx, n - 1)
if res < INF:
f[j + 1] = res + 1
return f[n]
T = int(input())
out = []
for _ in range(T):
out.append(str(solve()))
print("\n".join(out))
复杂度分析
时间复杂度:$O(n \log n)$ 空间复杂度:$O(n)$
小结
- 第一题(平稳风段):签到题,线性扫描维护当前段长度即可。
- 第二题(带调度的逻辑回归):考察 ML 工程实现能力。所有超参数锁死,需要完整实现标准化、Adam(含偏置修正)、Warm-up 调度和早停机制。关键是逐步骤精确对齐题目参数,不能自由发挥。
- 第三题(笨蛋同学):答案单调性引出二分框架。check 函数核心是分析”删一个元素 + 插一个新值”能否消除所有相邻差违例,需要枚举候选删除点并验证插入可行性。
- 第四题(弹性分桶):排序后连续段划分 DP。核心技巧是通过 $b[k] = a[k] - E \cdot k$ 变换解耦双下标约束,再用线段树维护滑动窗口内满足阈值的最小 $f$ 值,将 $O(n^2)$ 优化到 $O(n \log n)$。