大厂真题 / 上海AILab
上海AILab 5.26 笔试真题 - 通用岗
本场考试概述
考试时间:2026年5月26日 考试岗位:通用 难度评级:中等偏难(第三题数学推导难度较大)
考点分析:
- 第一题:榨汁机——模拟(难度简单)
- 第二题:栈中熔合——栈模拟 + GCD(难度中等)
- 第三题:排列mex计数——mex 单调性 + 最小覆盖窗口去重(难度困难)
建议策略:
- 前两题题面较长但思路明确,先按描述把状态机模拟清楚,再下手写代码
- 第三题需要先发现”$\text{mex} \geq v$ 等价于窗口包含 ${0, 1, \ldots, v-1}$”这一对偶,把问题转成最小覆盖窗口长度的去重计数
第一题:榨汁机
题目描述
有 $n$ 个水果,体积为 $a_1, a_2, \ldots, a_n$。按下标从小到大依次尝试将水果放入榨汁机,榨汁机一次榨汁前可承载的水果总体积至多为 $m$。
规则如下:
- 当准备放入的水果使机内容量之和超过 $m$ 时,将这个水果直接给贪吃鬼吃掉
- 随后若榨汁机里已有水果(机内总体积 $> 0$),则立刻启动一次榨汁并清空
- 所有水果处理完后,若机内仍有水果,还需再启动一次榨汁
求榨汁机启动的总次数,以及贪吃鬼吃掉的水果总体积。
样例
输入
5 10
3 4 5 6 2
输出
2 5
思路分析
第一步:理清状态转移
维护一个变量 $\text{cur}$ 表示当前机内总体积。对每个水果 $x$:
- 若 $\text{cur} + x > m$:水果被贪吃鬼吃掉(累加到被吃总量),同时若 $\text{cur} > 0$,启动一次榨汁并清空
- 否则:$\text{cur} \mathrel{+}= x$
第二步:处理尾部残留
遍历结束后若 $\text{cur} > 0$,再启动一次。
全程只需一次线性扫描,没有需要回溯或查询的结构。
题解代码
import sys
input = sys.stdin.readline
n, m = map(int, input().split())
a = list(map(int, input().split()))
cur = 0
eaten = 0
starts = 0
for x in a:
if cur + x > m:
eaten += x
if cur > 0:
starts += 1
cur = 0
else:
cur += x
if cur > 0:
starts += 1
print(starts, eaten)
复杂度分析
时间复杂度:$O(n)$ 空间复杂度:$O(n)$(存储输入数组)
第二题:栈中熔合
题目描述
给定正整数序列 $a_1, a_2, \ldots, a_n$ 和正整数阈值 $S$。从空栈开始,按顺序将 $a_i$ 依次压入栈中。
每次压入新元素后,执行如下熔合过程直至不能继续:
- 若栈顶元素为 $x$,其下方相邻元素为 $y$,并且 $x + y > S$,则将 $y$ 替换为 $\gcd(x, y)$ 并移除 $x$
- 重复检查直至栈中不存在相邻两元素之和严格大于 $S$ 的相邻对
处理完整个序列后,输出最终栈大小以及自栈底到栈顶的所有元素。
样例
输入
4 5
3 4 2 6
输出
2
1 2
思路分析
第一步:观察熔合的本质
每次熔合把两个元素变成一个 $\gcd$,值不会变大。熔合后新栈顶变小,可能使得它与更下方的元素之和不再超过 $S$,所以一轮循环检查即可稳定。
第二步:为什么直接循环就够?
关键观察:熔合后 $\gcd(x, y) \leq y$(原下方元素),因此新栈顶不会比它替换的那个 $y$ 更大。这意味着新栈顶与再下方元素的和 $\leq$ 原本 $y$ 与再下方元素的和——如果原本没触发熔合,现在也不会触发。所以每次压入后最多做一轮从栈顶向下的检查即可。
第三步:复杂度保证
每个元素最多入栈一次、出栈一次(被 GCD 替换掉时相当于出栈),总操作数为 $O(n)$。
题解代码
import sys
from math import gcd
input = sys.stdin.readline
n, S = map(int, input().split())
a = list(map(int, input().split()))
stack = []
for x in a:
stack.append(x)
while len(stack) >= 2 and stack[-1] + stack[-2] > S:
top = stack.pop()
stack[-1] = gcd(top, stack[-1])
print(len(stack))
print(*stack)
复杂度分析
时间复杂度:$O(n)$,每个元素至多入栈一次、出栈一次 空间复杂度:$O(n)$
第三题:排列mex计数
题目描述
给定长度为 $n$ 的排列 $a$(由 $0, 1, \ldots, n-1$ 构成),对 $1 \leq k \leq n$ 定义:
\[f(k) = \text{mex}(a_1, a_2, \ldots, a_k)\]求区间 $[1, n-1]$ 中有多少 $k$ 满足 $f(k) = f(k+1)$。
这里 $\text{mex}$ 是没有出现在数组中的最小非负整数。
样例
输入
5
2 3 1 0 4
输出
2
思路分析
第一步:观察 $f(k)$ 的单调性
随着 $k$ 增大,前缀包含的值越多,$\text{mex}$ 只能不降。所以 $f$ 是单调不减的。$f(k) = f(k+1)$ 意味着在位置 $k+1$ 没有发生跳变。
第二步:什么时候会发生跳变?
$f(k) < f(k+1)$ 当且仅当 $a_{k+1}$ 恰好等于当前的 $\text{mex}$ 值。但更本质的等价描述是:
长度为 $k$ 的前缀的 $\text{mex} \geq v$ 当且仅当前缀同时包含 ${0, 1, \ldots, v-1}$。
第三步:转化为最小覆盖窗口问题
设 $\text{pos}[v]$ 为值 $v$ 在排列中的下标。要让前缀包含 ${0, 1, \ldots, v-1}$(从而 $\text{mex} \geq v$),需要前缀长度 $\geq$ 这些值中最大下标 $+1$。
但题目问的是前缀(固定从位置 0 开始),所以包含 ${0, \ldots, v-1}$ 的最短前缀长度为:
\[L(v) = \max(\text{pos}[0], \text{pos}[1], \ldots, \text{pos}[v-1]) + 1\]$f$ 在位置 $L(v)$ 发生从 $< v$ 到 $\geq v$ 的跳变。跳变点的总数等于 $L(1), L(2), \ldots, L(n)$ 中不同值的个数。
第四步:答案
总共 $n-1$ 个位置对,减去跳变点数即为 $f(k) = f(k+1)$ 的个数:
\[\text{answer} = (n - 1) - \lvert\{L(1), L(2), \ldots, L(n)\}\rvert\]实现时只需要用一个变量追踪前缀最大下标,逐步加入值 $0, 1, \ldots, n-1$,将每步的 $L$ 值加入集合去重,最后用 $n-1$ 减去集合大小。
题解代码
import sys
input = sys.stdin.readline
n = int(input())
a = list(map(int, input().split()))
pos = [0] * n
for i, v in enumerate(a):
pos[v] = i
distinct_L = set()
mx = pos[0]
for v in range(1, n):
if pos[v] > mx:
mx = pos[v]
distinct_L.add(mx + 1)
print((n - 1) - len(distinct_L))
复杂度分析
时间复杂度:$O(n)$ 空间复杂度:$O(n)$
小结
- 第一题(榨汁机):纯模拟题,按规则维护机内状态即可,注意处理结束后的残留
- 第二题(栈中熔合):栈模拟 + GCD,关键洞察是 GCD 替换后值不增,不会引发向下方的连锁反应,保证了 $O(n)$ 总复杂度
- 第三题(排列mex计数):本场最难,核心是发现”$\text{mex}$ 跳变点 = 最小覆盖前缀长度的不同值”这一等价转化,把看似复杂的 mex 问题降维为一个线性扫描 + 集合去重