大厂真题 / 美团
美团 4.25 笔试真题 - 算法岗
本场考试概述
考试时间:2026年4月25日 考试岗位:算法岗 难度评级:中等偏难
考点分析:
- 第一题:贪心 + 字符串(难度中等)
- 第二题:Decision Stump 决策树桩(难度中等,ML 实现题)
- 第三题:按位贪心(难度困难)
- 第四题:树上哈希 + 小到大合并(难度困难)
建议策略:
- 前两道题是拿分关键——第一题贪心需要观察字符镜像操作对前后半字母表的不同效果,第二题是经典 ML 实现题
- 第三题是异或类问题的通用套路:按位拆分后独立处理,掌握后遇到类似题能快速切入
- 第四题是难题,涉及树上路径哈希和启发式合并,建议留到最后
第一题:镜像串
题目描述
给定一个仅由小写英文字母组成的字符串 $s$。可以选择至多一次,对任意一个连续区间 $[l, r]$ 执行”镜像”操作:将区间内每个字符 $c$ 替换为 $c’ = \text{chr}(\text{ord}(\text{‘a’}) + \text{ord}(\text{‘z’}) - \text{ord}(c))$。
即 $\text{‘a’} \leftrightarrow \text{‘z’}$,$\text{‘b’} \leftrightarrow \text{‘y’}$,$\text{‘c’} \leftrightarrow \text{‘x’}$,以此类推。
也可以选择不进行任何操作。要求在至多一次操作后,使字符串按字典序尽可能小,输出该最小结果。
多组测试数据,保证所有字符串长度之和不超过 $10^5$。
样例
输入
5
abcxyz
aaaa
zyx
az
b
输出
abccba
aaaa
abc
aa
b
思路分析
第一步:观察镜像操作对单个字符的影响
26 个字母以 $\text{‘n’}$ 为分界线分成两半:
- 前半 $\text{‘a’}$–$\text{‘m’}$:镜像后变成 $\text{‘z’}$–$\text{‘n’}$,字典序变大
- 后半 $\text{‘n’}$–$\text{‘z’}$:镜像后变成 $\text{‘m’}$–$\text{‘a’}$,字典序变小
所以镜像只对后半段字母有好处,前半段字母一旦被纳入区间只会变大。
第二步:贪心策略
从左到右扫描,遇到 $\text{‘a’}$–$\text{‘m’}$ 不动(它们已经足够小),找到第一个 $\geq \text{‘n’}$ 的字符开始镜像,一路向右直到再次遇到 $< \text{‘n’}$ 的字符时停下。
为什么不能跳过中间的 $< \text{‘n’}$ 去镜像后面的 $\geq \text{‘n’}$?因为题目要求区间连续,跳不过去——中间的小字母被镜像后会变大,得不偿失。
如果整个字符串没有 $\geq \text{‘n’}$ 的字符,就不做任何操作。
题解代码
import sys
input = sys.stdin.readline
T = int(input())
for _ in range(T):
s = list(input().strip())
n = len(s)
i = 0
while i < n and s[i] < 'n':
i += 1
while i < n and s[i] >= 'n':
s[i] = chr(ord('a') + ord('z') - ord(s[i]))
i += 1
print(''.join(s))
复杂度分析
时间复杂度:$O(n)$,每个字符最多访问一次。 空间复杂度:$O(n)$,存储字符数组。
第二题:AK机的决策树桩
题目描述
手写实现一个二分类的 Decision Stump(深度 = 1 的决策树),只使用 NumPy。模型遍历每个特征,寻找单一阈值划分,使得加权 Gini 不纯度最小。
训练阶段:
- 输入
train为二维列表,最后一列为标签 $y \in {0, 1}$,前面为数值型特征 - 对每个特征:按该特征升序稳定排序;在所有相邻不同值中点及”最小值 $- \varepsilon$”处设阈值候选;计算每个候选阈值处的加权 Gini
- 在所有特征最优阈值中选全局最小 Gini——若并列:先取特征索引最小,再取阈值最小
- 左子集($\leq$ 阈值)和右子集($>$ 阈值)各取多数类作为预测;若 0/1 数量相等则输出 0;若子集为空则跳过该阈值
预测阶段:对每条测试样本,$\leq$ 阈值预测左叶类别,否则预测右叶类别。
输入为单行 JSON,输出为预测标签的 JSON 数组。
样例
输入
{"train": [[0,0],[1,0],[2,0],[3,1],[4,1],[5,1]],"test": [[-1],[2.5],[4]]}
输出
[0, 0, 1]
思路分析
第一步:理解 Decision Stump
Decision Stump 就是只切一刀的决策树:选一个特征、定一个阈值,把样本分成两堆,让每堆尽量”纯”(同一类的尽量待在一起)。
第二步:搜索空间
两层循环:外层遍历 $m$ 个特征,内层遍历该特征上的候选阈值。$n, m \leq 1000$,暴力穷举完全够用。
第三步:候选阈值构造
对第 $j$ 个特征,把样本按特征值排序去重得到 $v_1, v_2, \dots, v_k$,候选阈值取:
- $v_1 - \varepsilon$(把所有样本划到右边的极端情况)
- 相邻不同值的中点 $\frac{v_i + v_{i+1}}{2}$
第四步:加权 Gini 计算
Gini 不纯度衡量样本的”混乱”程度。全是同一类 → Gini = 0(最纯);一半 0 一半 1 → Gini = 0.5(最混乱)。
对阈值 $t$,左子集 $L$($\leq t$)和右子集 $R$($> t$),加权 Gini 为:
\[G = \frac{n_L}{n} \cdot g_L + \frac{n_R}{n} \cdot g_R\]其中 $g = 1 - p_0^2 - p_1^2$,$p_i$ 是子集中类别 $i$ 的比例。某侧为空时跳过。
第五步:全局选优
所有 $(G, j, t)$ 中取最小的——元组比较天然满足”先比 Gini,再比特征编号,再比阈值”的并列消歧规则。
题解代码
import json
import numpy as np
import sys
raw = sys.stdin.readline()
data = json.loads(raw)
train = np.array(data["train"], dtype=float)
test_data = np.array(data["test"], dtype=float)
X = train[:, :-1]
y = train[:, -1].astype(int)
n, m = X.shape
eps = 1e-7
best = (float('inf'), -1, float('inf'))
for j in range(m):
idx = np.argsort(X[:, j], kind='stable')
vals = X[idx, j]
labels = y[idx]
unique_vals = np.unique(vals)
thresholds = [unique_vals[0] - eps]
for i in range(len(unique_vals) - 1):
thresholds.append((unique_vals[i] + unique_vals[i + 1]) / 2.0)
for thresh in thresholds:
mask = vals <= thresh
n_L = int(np.sum(mask))
n_R = n - n_L
if n_L == 0 or n_R == 0:
continue
left_y = labels[mask]
right_y = labels[~mask]
g_L = 1.0 - (np.sum(left_y == 0) / n_L) ** 2 - (np.sum(left_y == 1) / n_L) ** 2
g_R = 1.0 - (np.sum(right_y == 0) / n_R) ** 2 - (np.sum(right_y == 1) / n_R) ** 2
G = (n_L / n) * g_L + (n_R / n) * g_R
if (G, j, thresh) < best:
best = (G, j, thresh)
best_feat = best[1]
best_thresh = best[2]
left_y = y[X[:, best_feat] <= best_thresh]
right_y = y[X[:, best_feat] > best_thresh]
def majority(labels):
c0 = int(np.sum(labels == 0))
c1 = int(np.sum(labels == 1))
return 0 if c0 >= c1 else 1
left_pred = majority(left_y)
right_pred = majority(right_y)
preds = []
for t in test_data:
preds.append(left_pred if t[best_feat] <= best_thresh else right_pred)
print(json.dumps(preds))
复杂度分析
时间复杂度:$O(m \cdot n \log n)$,对 $m$ 个特征各排序一次 $O(n \log n)$,再遍历 $O(n)$ 个候选阈值。 空间复杂度:$O(mn)$,存储特征矩阵。
第三题:AK机的异或问题
题目描述
给定长度为 $n$ 的数组 $a$ 和整数 $k$。可以执行任意次操作:选择下标 $i$ 和整数 $x$(满足 $0 \leq x \leq k$),将 $a_i$ 赋值为 $a_i \oplus x$($\oplus$ 为按位异或),代价为 $x$。
需要最大化操作后所有两两异或和 $\sum_{i < j} (a_i \oplus a_j)$。若有多种操作序列使该值最大化,选择总代价最小的。输出最大值和最小代价。
样例
输入
3 3
0 1 9
输出
22 2
思路分析
第一步:按位拆分
异或是逐位运算,第 $b$ 位的操作不影响第 $b’$ 位。所以两两异或和也可以按位拆开算:第 $b$ 位的贡献是 $c_0 \cdot c_1 \cdot 2^b$,其中 $c_0$、$c_1$ 分别是第 $b$ 位上 0 和 1 的个数。一对元素在第 $b$ 位上产生贡献,当且仅当这一位不同(一个 0、一个 1),这样的配对数恰好是 $c_0 \cdot c_1$。
第二步:单位贡献最大化
$c_0 \cdot c_1$(其中 $c_0 + c_1 = n$)是一个开口朝下的抛物线,顶点在 $c_1 = n/2$ 处,0 和 1 的个数尽量均分时乘积最大。目标是让每一位都尽量均分。
第三步:可翻转判定
- 若 $2^b \leq k$:可以用 $x = 2^b$ 单独翻转该位而不影响其他位,一定能达到均分。需要翻转的个数是 $\lvert c_1 - \lfloor n/2 \rfloor \rvert$,每次代价 $2^b$。
- 若 $2^b > k$:翻不了,该位的贡献固定为原始的 $c_0 \cdot c_1 \cdot 2^b$。
第四步:代价最优性
逐位各翻一次的代价恰好等于最小代价——对同一个元素做多次操作,净效果是异或一个值,但代价是各次 $x$ 之和(不小于逐位翻转的代价)。
以样例 $a = [0, 1, 9]$,$k = 3$ 走一遍:
- 第 0 位:$c_1 = 2$,已均分,贡献 $1 \times 2 \times 1 = 2$
- 第 1 位:$c_1 = 0$,全是 0,需翻 1 个,代价 $1 \times 2 = 2$,贡献 $1 \times 2 \times 2 = 4$
- 第 2 位:$c_1 = 1$,已均分,贡献 $2 \times 1 \times 4 = 8$
- 第 3 位:$c_1 = 1$,$2^3 = 8 > 3$,翻不了,贡献 $2 \times 1 \times 8 = 16$(不是均分后的值)
等等,让我重算。$a = [0, 1, 9]$,$9 = 1001_2$,$1 = 0001_2$,$0 = 0000_2$。
- 第 0 位:bits = [0, 1, 1],$c_1 = 2$,$c_0 = 1$。均分目标 $\lfloor 3/2 \rfloor = 1$,$\lceil 3/2 \rceil = 2$,已在范围内。贡献 $1 \times 2 \times 1 = 2$。
- 第 1 位:bits = [0, 0, 0],$c_1 = 0$。$2^1 = 2 \leq 3$,可翻。翻 1 个到 $c_1 = 1$。代价 $1 \times 2 = 2$。贡献 $1 \times 2 \times 2 = 4$。
- 第 2 位:bits = [0, 0, 0],$c_1 = 0$。$2^2 = 4 > 3$,翻不了。贡献 $0$。
- 第 3 位:bits = [0, 0, 1],$c_1 = 1$。$2^3 = 8 > 3$,翻不了。贡献 $2 \times 1 \times 8 = 16$。
总和 = $2 + 4 + 0 + 16 = 22$,代价 = $2$。与样例输出一致。
题解代码
import sys
input = sys.stdin.readline
n, k = map(int, input().split())
a = list(map(int, input().split()))
half_lo = n // 2
half_hi = (n + 1) // 2
max_sum = 0
min_cost = 0
for b in range(30):
cnt1 = sum(1 for x in a if (x >> b) & 1)
cnt0 = n - cnt1
if (1 << b) <= k:
if cnt1 < half_lo:
flips = half_lo - cnt1
elif cnt1 > half_hi:
flips = cnt1 - half_hi
else:
flips = 0
min_cost += flips * (1 << b)
max_sum += half_lo * half_hi * (1 << b)
else:
max_sum += cnt0 * cnt1 * (1 << b)
print(max_sum, min_cost)
复杂度分析
时间复杂度:$O(n \log V)$,遍历 30 个位,每位统计 $O(n)$。 空间复杂度:$O(n)$,存储输入数组。
第四题:树上操作
题目描述
给定一棵 $n$ 个节点的有根树(根节点为 1),第 $i$ 个节点上有一个小写字母 $s_i$。
对每个节点 $u$,定义权值 $\text{val}(u)$:从以 $u$ 为根的子树中选两个节点 $x, y$(可以相同),满足 $\text{LCA}(x, y) = u$,且 $x$ 到 $y$ 的简单路径上的字母拼接后是回文串。在所有满足条件的 $(x, y)$ 中,$\text{val}(u)$ 等于路径点数的最大值。如果不存在这样的配对,$\text{val}(u) = 1$(选 $x = y = u$)。
输出每个节点的 $\text{val}(u)$。
$n \leq 10^5$。
样例
输入
7
aaaaaaa
1 2
1 3
2 4
2 5
3 6
3 7
输出
5 3 3 1 1 1 1
思路分析
第一步:回文路径的结构
路径 $x \to \text{LCA} \to y$ 以 $u$ 为中心、两侧等长。左半是从 $u$ 向 $x$ 方向走的字符序列,右半是从 $u$ 向 $y$ 方向走的字符序列。回文要求正读等于倒读,中心 $u$ 本身在奇数长度的正中间没有额外约束,所以条件就是:左半和右半逐层字符相同。
以样例为例(所有节点都是 ‘a’):从节点 1 向左走两步(1→2→4)字符 “aa”,向右走两步(1→3→6)也是 “aa”,每层一样,回文成立。路径长度为 $2 \times 2 + 1 = 5$。
第二步:路径哈希
给每个节点 $v$ 算一个指纹 $h(v)$,把从根到 $v$ 经过的全部字符压缩成一个数字(多项式哈希):
\[h(v) = h(\text{par}(v)) \times B + (\text{ord}(s_v) - 96) \pmod{M}\]两个同深度节点 $x$、$y$ 的 $h$ 值相等,说明从根到它们走过的字符完全一样。如果它们还在节点 $u$ 的不同子树里,根到 $u$ 这段是共享前缀,$u$ 往下那段逐层字符必然相同——回文条件满足。
第三步:小到大合并
自底向上处理。每个节点维护一个集合,存子树内所有后代的 $(depth, hash)$。合并时,碰到两个来自不同子树、深度相同且哈希相同的后代,就找到了一对合法的 $(x, y)$,回文路径长度为 $2 \times (depth - dep_u) + 1$。
关键优化:保留最大的子树集合不动,只把较小子树的元素逐个搬进去。这样每个元素每次被搬后所在集合至少翻倍,一个元素一生最多被搬 $O(\log n)$ 次,总搬运量 $O(n \log n)$。
题解代码
import sys
from collections import deque
input = sys.stdin.readline
n = int(input())
s = input().strip()
if n == 1:
print(1)
else:
adj = [[] for _ in range(n)]
for _ in range(n - 1):
u, v = map(int, input().split())
adj[u - 1].append(v - 1)
adj[v - 1].append(u - 1)
dep = [-1] * n
par = [-1] * n
dep[0] = 0
queue = deque([0])
order = []
while queue:
u = queue.popleft()
order.append(u)
for v in adj[u]:
if dep[v] == -1:
dep[v] = dep[u] + 1
par[v] = u
queue.append(v)
BASE, MOD = 131, 10 ** 9 + 7
h = [0] * n
h[0] = ord(s[0]) - 96
for u in order[1:]:
h[u] = (h[par[u]] * BASE + ord(s[u]) - 96) % MOD
children = [[] for _ in range(n)]
for u in order[1:]:
children[par[u]].append(u)
val = [1] * n
node_map = [None] * n
for u in reversed(order):
if not children[u]:
node_map[u] = {dep[u] * MOD + h[u]}
continue
heaviest = max(children[u], key=lambda c: len(node_map[c]) if node_map[c] else 0)
merged = node_map[heaviest]
node_map[heaviest] = None
for c in children[u]:
if c == heaviest:
continue
cm = node_map[c]
if cm is None:
continue
for key in cm:
if key in merged:
d = key // MOD
val[u] = max(val[u], 2 * (d - dep[u]) + 1)
merged.update(cm)
node_map[c] = None
merged.add(dep[u] * MOD + h[u])
node_map[u] = merged
print(' '.join(map(str, val)))
复杂度分析
时间复杂度:$O(n \log n)$,小到大合并保证每个节点被移动 $O(\log n)$ 次,哈希集合的查找和插入均摊 $O(1)$。 空间复杂度:$O(n)$,BFS 数组和哈希集合的总条目数为 $O(n)$。
小结
- 第一题是贪心字符串题,核心是观察镜像操作对字母表前后半的不对称效果——前半变大、后半变小,找到第一段后半字母连续镜像即可
- 第二题是 ML 实现题(Decision Stump),按题意模拟即可,关键是理清候选阈值的构造规则和加权 Gini 的计算公式
- 第三题利用异或的按位独立性将问题分解——每一位独立最优化,能翻就均分,翻不了就保持原样
- 第四题是树上算法难题,将回文路径条件转化为”路径哈希相等”的判定,再用启发式合并高效处理子树间的配对查找