大厂真题 / 得物
得物 4.18 笔试真题 - 暑期实习
本场考试概述
考试时间:2026年4月18日 考试岗位:暑期实习(通用) 难度评级:中等偏难
考点分析:
- 第一题:状态压缩 + 记忆化搜索(难度困难)
- 第二题:状态机 DP(难度中等)
- 第三题:BFS + 优先队列 / 接雨水 2D(难度中等)
建议策略:
- 第二题状态机 DP 是经典模型,建议优先拿满分
- 第三题是 Trapping Rain Water II 变体,熟悉模板可快速 AC
- 第一题 n <= 15 暗示状态压缩,需要仔细设计状态和转移,考场上能理清收益分配逻辑就很好
第一题:栈的统计
题目描述
给定两个长度为 n 的数组 a 和 b。有一个初始为空的栈,需要进行 2n 次操作,每次操作为以下两种之一:
- 操作 1:将数组 a 中尚未入栈的下一个元素压入栈中(按 a[0], a[1], …, a[n-1] 的顺序依次入栈)。
- 操作 2:弹出栈顶元素,获得收益 b[当前栈大小 - 1] * a[栈顶元素]。
要求所有元素必须恰好入栈一次、出栈一次(即 2n 次操作后栈为空)。求所有合法操作方案的总收益之和。
n <= 15。
样例
输入
2
1 2
2 3
输出
14
思路分析
第一步:理解操作语义
操作序列本质上是一个”入栈-出栈”的调度方案。n 个元素必须按顺序入栈(a[0] 先于 a[1] 先于 a[2]…),但出栈时机可以灵活选择——只要栈非空就可以弹出。每次弹出时,收益取决于当时栈的大小和弹出的元素值。
例如 n=2 时,合法方案有两种:
- push(a[0]), pop(a[0]), push(a[1]), pop(a[1]):收益 = b[0]a[0] + b[0]a[1] = 21 + 22 = 6
- push(a[0]), push(a[1]), pop(a[1]), pop(a[0]):收益 = b[1]a[1] + b[0]a[0] = 32 + 21 = 8
总收益 = 6 + 8 = 14。
第二步:状态设计——用 bitmask 表示栈内元素
因为入栈顺序固定(a[0] 到 a[n-1]),我们可以用一个变量 pushed 记录当前已经入栈了多少个元素,用一个 bitmask mask 记录当前哪些元素仍在栈中(第 i 位为 1 表示 a[i] 在栈中)。
状态 (pushed, mask) 唯一确定了当前的局面。注意 mask 中为 1 的位一定都 < pushed(只有已入栈的元素才可能在栈中)。
第三步:记忆化搜索的返回值
定义 dfs(pushed, mask) 返回一个二元组 (ways, total_benefit):
ways:从当前状态到终态(pushed=n, mask=0)的合法操作方案数total_benefit:所有这些方案的收益总和
终态:pushed == n 且 mask == 0 时,返回 (1, 0)。
第四步:转移——push 和 pop 两种选择
从状态 (pushed, mask):
-
Push 操作(条件:pushed < n):将 a[pushed] 入栈,转移到 (pushed+1, mask (1«pushed))。这步操作本身没有收益。 - Pop 操作(条件:mask != 0):弹出栈顶元素。栈顶是 mask 中编号最大的那一位(因为入栈顺序递增,后入的编号更大,且栈是后进先出)。设栈顶位置为 top,当前栈大小为 popcount(mask),收益为
gain = b[popcount(mask) - 1] * a[top]。转移到 (pushed, mask ^ (1«top))。
对于 pop 操作,后续子问题返回 (ways_after, benefit_after),那么:
- 方案数贡献:ways_after
- 收益贡献:ways_after * gain + benefit_after(每条后续方案都会累加当前 pop 的收益)
第五步:汇总两个分支
将 push 分支和 pop 分支的 (ways, benefit) 分别累加,得到当前状态的总方案数和总收益。
第六步:复杂度验证
状态数:pushed 有 n+1 种取值,mask 最多 2^n 种。但有效状态远少于 (n+1) * 2^n,因为 mask 中的 1 位必须 < pushed。总有效状态约 O(3^n)(每个元素三种状态:未入栈、在栈中、已出栈)。对于 n=15,3^15 约 1430 万,加上 lru_cache 的开销,在时限内可行。
每个状态的转移中,找栈顶需要 O(n) 扫描,因此总时间 O(n * 3^n)。
题解代码
import sys
from functools import lru_cache
input = sys.stdin.readline
def solve():
n = int(input())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
@lru_cache(maxsize=None)
def dfs(pushed, mask):
# 终态:所有元素已入栈且已出栈
if pushed == n and mask == 0:
return (1, 0)
total_ways, total_benefit = 0, 0
# 选择1:Push 下一个元素
if pushed < n:
ways, benefit = dfs(pushed + 1, mask | (1 << pushed))
total_ways += ways
total_benefit += benefit
# 选择2:Pop 栈顶元素
if mask != 0:
# 栈顶是 mask 中最高位(最后入栈的)
top = -1
for i in range(pushed - 1, -1, -1):
if mask & (1 << i):
top = i
break
# 当前栈大小 = popcount(mask)
x = bin(mask).count('1')
gain = b[x - 1] * a[top]
ways, benefit = dfs(pushed, mask ^ (1 << top))
total_ways += ways
# 每条后续方案都累加当前 pop 的收益
total_benefit += ways * gain + benefit
return (total_ways, total_benefit)
_, ans = dfs(0, 0)
print(ans)
solve()
复杂度分析
时间复杂度:O(n * 3^n)。有效状态数约 3^n(每个元素三种状态),每个状态内找栈顶需 O(n)。n=15 时约 2100 万次操作。 空间复杂度:O(3^n),记忆化缓存存储所有有效状态。
第二题:爬山
题目描述
有 n 天可以爬山,每天爬山能获得 a[i] 的快乐值。爬山规则:如果某天爬了山,那么第二天必须休息,除非使用一次”例外机会”跳过休息。总共有 k 次例外机会可以使用。求能获得的最大快乐值总和。
样例
输入
7 1
1 2 3 4 5 6 7
输出
19
思路分析
第一步:理解约束
正常情况下,如果第 i 天爬山,第 i+1 天必须休息(不能连续两天爬山)。但如果使用一次例外机会,就可以连续爬山(即第 i 天和第 i+1 天都爬山)。总共可以使用 k 次例外。
这是一个典型的”带资源限制的选择问题”,适合用状态机 DP 解决。
第二步:状态定义
定义 f[i][j][s]:考虑前 i 天,已使用 j 次例外机会,第 i 天状态为 s 时的最大快乐值。
状态 s 取值:
- s = 0:第 i 天休息
- s = 1:第 i 天爬山
第三步:状态转移
对于第 i 天休息(s=0):
- 前一天可以是任意状态(休息或爬山都行,因为休息不受限制)
f[i][j][0] = max(f[i-1][j][0], f[i-1][j][1])
对于第 i 天爬山(s=1),有两种情况:
- 前一天休息(正常爬山,不消耗例外):
f[i-1][j][0] + a[i] - 前一天也爬山(连续爬山,消耗一次例外):
f[i-1][j-1][1] + a[i](需要 j >= 1)
f[i][j][1] = max(f[i-1][j][0], f[i-1][j-1][1] if j >= 1) + a[i]
第四步:初始化和答案
初始化:f[0][0][0] = 0(第 0 天休息,0 次例外,快乐值为 0)。其余为负无穷。
如果下标从 1 开始:
f[1][0][0] = 0(第 1 天休息)f[1][0][1] = a[1](第 1 天爬山)
答案:max(f[n][j][s]) 对所有 j <= k, s in {0,1}。
第五步:验证样例
n=7, k=1, a=[1,2,3,4,5,6,7]。
不使用例外的最优:选第 1,3,5,7 天,快乐值 = 1+3+5+7 = 16。 使用 1 次例外:可以让某两天连续。最优是选第 2,3,5,7 天(第 2 和 3 天连续,消耗 1 次例外),快乐值 = 2+3+5+7 = 17。或者选第 1,3,5,6,7 天(第 6 和 7 天连续),快乐值 = 1+3+5+6+7 = 22?
等等,让我重新分析。使用例外可以连续爬山,意味着可以多选一天。最优应该是选尽量多且大的值。选第 3,4,5,6,7 天:第 3-4 连续(1次例外),第 4-5 连续(又要1次例外),超出了。
k=1 只能有 1 对连续。最优:选第 1,3,5,6,7 天——第 5-6 连续用 1 次例外,第 6-7 连续又需要 1 次。不行。
实际只能有 1 次连续:选第 1,3,5,7 天加上第 6 天,需要第 5-6 连续消耗例外,但第 6-7 又连续需要再一次。不可行。
正确理解:选第 1,3,4,6 天:第 3-4 连续用 1 次例外,快乐值 = 1+3+4+6 = 14。或选第 2,4,5,7 天:第 4-5 连续,快乐值 = 2+4+5+7 = 18。或选第 1,3,5,6 天:第 5-6 连续,快乐值 = 1+3+5+6 = 15。或选第 2,4,6,7 天:第 6-7 连续,快乐值 = 2+4+6+7 = 19。
答案 19,与样例一致。
第六步:空间优化
由于第 i 天只依赖第 i-1 天,可以用滚动数组将空间从 O(nk) 优化到 O(k)。但 O(nk) 通常已足够。
题解代码
import sys
input = sys.stdin.readline
def solve():
n, k = map(int, input().split())
a = list(map(int, input().split()))
# f[j][s]: 到当前天为止,用了 j 次例外,当天状态 s 的最大快乐值
# s=0 休息,s=1 爬山
NEG_INF = float('-inf')
# 初始化:第 0 天之前,j=0, 休息状态,快乐值 0
prev = [[NEG_INF, NEG_INF] for _ in range(k + 1)]
prev[0][0] = 0
for i in range(n):
cur = [[NEG_INF, NEG_INF] for _ in range(k + 1)]
for j in range(k + 1):
# 当天休息:前一天任意状态
val = max(prev[j][0], prev[j][1])
if val > cur[j][0]:
cur[j][0] = val
# 当天爬山
# 情况1:前一天休息(不消耗例外)
if prev[j][0] != NEG_INF:
val = prev[j][0] + a[i]
if val > cur[j][1]:
cur[j][1] = val
# 情况2:前一天爬山(消耗 1 次例外,即从 j-1 转移)
if j >= 1 and prev[j - 1][1] != NEG_INF:
val = prev[j - 1][1] + a[i]
if val > cur[j][1]:
cur[j][1] = val
prev = cur
ans = 0
for j in range(k + 1):
ans = max(ans, prev[j][0], prev[j][1])
print(ans)
solve()
复杂度分析
时间复杂度:O(nk),双层循环遍历天数和例外使用次数。 空间复杂度:O(k),滚动数组只保留前一天的状态。
第三题:防水建材
题目描述
给定一个 n x m 的高度矩阵,表示一片区域的地面高度。下雨后,水会在低洼处积聚。边缘格子的水会流出,因此边缘格子无法积水。求雨后形成的独立水池数量(即被积水的连续区域构成的连通分量个数)。
样例
输入
4 4
2 3 5 1
4 1 2 3
1 5 4 2
1 2 2 2
输出
1
思路分析
第一步:问题本质——接雨水 II 的变体
这是经典的 Trapping Rain Water II(LeetCode 407)的变体。在二维地形上,水能不能积住取决于该区域周围的”围墙”是否足够高。具体来说,一个格子能积水当且仅当从它出发的所有路径到达边界前,都必须经过比它高的格子。
积水高度 = min(所有路径到边界需翻越的最高”矮墙”) - 格子自身高度。如果这个值 > 0,说明该格子被淹没。
第二步:最小堆 BFS 确定每个格子的水位
使用优先队列(最小堆)从边界向内扩展:
- 将所有边界格子加入最小堆,它们的水位等于自身高度(边界不能积水)
- 标记所有边界格子为已访问
- 每次从堆中取出水位最小的格子 (water_level, x, y)
- 遍历其四个邻居:若邻居未访问,则邻居的水位 = max(water_level, 邻居自身高度)
- 若邻居自身高度 >= water_level,水流不过来,邻居水位就是自身高度
- 若邻居自身高度 < water_level,水会漫过来淹没它,邻居水位等于 water_level
- 将邻居加入堆,标记已访问
这个过程保证了每个格子被确定水位时,用的是”最优路径”——即从边界到它的所有路径中,瓶颈最小的那条。
第三步:判断哪些格子被淹没
处理完所有格子后,如果某格子的水位 > 自身高度,说明它被水覆盖了。将所有被淹没的格子标记出来。
第四步:BFS 统计连通分量
对所有被淹没的格子做连通分量计数:遍历矩阵,遇到未统计的被淹格子就启动一次 BFS/DFS,将相邻的被淹格子归为同一个水池。最终连通分量的数量即为独立水池数。
第五步:验证样例
2 3 5 1
4 1 2 3
1 5 4 2
1 2 2 2
边界格子:第一行、最后一行、第一列、最后一列的所有格子。
从边界向内扩展后,内部格子 (1,1) 高度为 1,周围边界最低为 2(来自左边的格子 (1,0)=4 或上方 (0,1)=3 等路径)。经过堆 BFS 确定后,(1,1) 的水位 = 2(由周围路径的最小瓶颈决定),自身高度 1 < 2,被淹没。
格子 (1,2) 高度为 2,水位也为 2(或恰好等于自身),不被淹没。
经过完整计算,只有 (1,1) 被淹没(水位 2 > 自身高度 1),构成 1 个独立水池。输出 1。
题解代码
import sys
import heapq
from collections import deque
input = sys.stdin.readline
def solve():
n, m = map(int, input().split())
grid = []
for _ in range(n):
grid.append(list(map(int, input().split())))
# Step 1: 最小堆 BFS 确定每个格子的水位
water = [[0] * m for _ in range(n)]
visited = [[False] * m for _ in range(n)]
heap = []
# 将边界格子加入堆
for i in range(n):
for j in range(m):
if i == 0 or i == n - 1 or j == 0 or j == m - 1:
heapq.heappush(heap, (grid[i][j], i, j))
visited[i][j] = True
water[i][j] = grid[i][j]
dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
while heap:
wl, x, y = heapq.heappop(heap)
for dx, dy in dirs:
nx, ny = x + dx, y + dy
if 0 <= nx < n and 0 <= ny < m and not visited[nx][ny]:
visited[nx][ny] = True
# 邻居水位 = max(当前水位, 邻居自身高度)
water[nx][ny] = max(wl, grid[nx][ny])
heapq.heappush(heap, (water[nx][ny], nx, ny))
# Step 2: 标记被淹没的格子
flooded = [[False] * m for _ in range(n)]
for i in range(n):
for j in range(m):
if water[i][j] > grid[i][j]:
flooded[i][j] = True
# Step 3: BFS 统计连通分量数
count = 0
for i in range(n):
for j in range(m):
if flooded[i][j]:
count += 1
# BFS 标记整个连通分量
queue = deque()
queue.append((i, j))
flooded[i][j] = False
while queue:
cx, cy = queue.popleft()
for dx, dy in dirs:
nx, ny = cx + dx, cy + dy
if 0 <= nx < n and 0 <= ny < m and flooded[nx][ny]:
flooded[nx][ny] = False
queue.append((nx, ny))
print(count)
solve()
复杂度分析
时间复杂度:O(nm log(nm))。堆 BFS 中每个格子入堆出堆各一次,每次操作 O(log(nm));后续连通分量 BFS 为 O(nm)。 空间复杂度:O(nm),存储水位矩阵、访问标记和堆。
小结
- 第一题是状态压缩 + 记忆化搜索的典型题,核心难点在于状态设计:用 bitmask 精确描述栈内容,dfs 返回 (方案数, 总收益) 二元组来正确分配每步 pop 的收益贡献
- 第二题状态机 DP 是面试高频题型,关键在于正确定义”前一天爬山 + 今天爬山”需消耗一次例外机会的转移关系,滚动数组优化空间
- 第三题是 Trapping Rain Water II 的经典拓展,最小堆 BFS 从边界向内确定水位是核心模板,再用 BFS 统计被淹格子的连通分量即为水池数量