大厂真题 / 华为
华为 5.20 笔试真题 - 研发岗
本场考试概述
考试时间:2026年5月20日 考试岗位:研发岗(国内) 难度评级:中等偏难
考点分析:
- 第一题:二分答案(难度中等)
- 第二题:二叉树前序+中序重建 + 路径和剪枝 + 单子节点链合并(难度中等偏难)
- 第三题:树形依赖背包(难度困难)
建议策略:
- 第一题是二分答案模板题,识别”答案具有单调性 + 给定答案可以 check 可行性”的模式后快速切入,建议优先拿下
- 第二题先稳扎稳打把二叉树重建模板写出来,再逐步加上剪枝和合并逻辑;特别注意规则一判断的是”路径和”而非节点自身值
- 第三题是树形依赖背包,若没练过该类型可暂时跳过,先保前两题分数
第一题:服务器处理计算任务
题目描述
服务器集群中有 $n$ 个待处理的计算任务,第 $i$ 个任务需要的总计算量为 $\text{tasks}_i$。所有任务必须在 $h$ 小时内完成。
需要决定同时启用 $k$ 台服务器。系统按小时运行,每台服务器每小时可处理 $1$ 单位计算量,处理规则如下:
- 集中处理:每小时必须从剩余任务中挑选一个,并将全部 $k$ 台服务器都投入到该任务上进行计算。所有服务器同一小时内只能处理同一任务。
- 资源闲置:如果当前被选中的任务所需剩余计算量小于 $k$,那么多余的服务器在该小时内将处于闲置状态,该任务仍视为在该小时内完成。
请计算并返回最少需要同时启用多少台服务器,才能确保在 $h$ 小时内完成所有任务。如果无法完成,返回 $-1$。
输入描述
第一行输入整数 $n$。第二行输入 $n$ 个正整数 $\text{tasks}_i$。第三行输入整数 $h$。
输出描述
输出一个整数,表示至少需要的服务器数量;无解输出 $-1$。
样例
输入
5
30 11 23 4 20
5
输出
30
启用 $30$ 台服务器时每小时可完成一个任务,共 $5$ 小时刚好满足。
输入
4
3 6 7 11
8
输出
4
启用 $4$ 台服务器,处理各任务分别需要 $1, 2, 2, 3$ 小时,总计 $8$ 小时。
思路分析
第一步:观察题目性质
题目要求”最少的服务器数 $k$”,使得所有任务在 $h$ 小时内完成。每个任务的耗时为 $\lceil \text{tasks}_i / k \rceil$,所有任务耗时之和不超过 $h$ 即可。
关键观察:$k$ 越大,每个任务完成得越快,总耗时单调不增。这是典型的答案具有单调性的场景——适合二分答案。
第二步:设计 check 函数
给定服务器数 $k$,计算所有任务的总耗时:
\[\sum_{i=1}^{n} \lceil \text{tasks}_i / k \rceil\]如果总耗时 $\leq h$,则 $k$ 台服务器够用。
第三步:处理边界
- 如果 $n > h$:每个任务至少占 $1$ 小时,$n$ 个任务至少 $n$ 小时,无解返回 $-1$
- 如果所有任务计算量之和 $\leq h$:每小时处理 $1$ 单位就够,$k=1$ 即可
第四步:二分区间
- 下界 $lo = 1$
- 上界 $hi = \max(\text{tasks}_i)$(此时每个任务都能 $1$ 小时完成)
check 通过则 $hi = mid$,否则 $lo = mid + 1$。
题解代码
import sys
input = sys.stdin.readline
def solve():
n = int(input())
tasks = list(map(int, input().split()))
h = int(input())
if n > h:
print(-1)
return
total = sum(tasks)
if total <= h:
print(1)
return
max_t = max(tasks)
def check(k):
hours = 0
for t in tasks:
hours += (t + k - 1) // k
if hours > h:
return False
return True
lo, hi = 1, max_t
while lo < hi:
mid = (lo + hi) // 2
if check(mid):
hi = mid
else:
lo = mid + 1
print(lo)
solve()
复杂度分析
时间复杂度:$O(n \log M)$,其中 $M = \max(\text{tasks}_i)$
空间复杂度:$O(n)$
第二题:容器镜像树精简
题目描述
在容器镜像管理系统中,镜像采用二叉树管理。每个节点描述镜像层大小,节点的镜像完整大小为该节点值与其所有祖先节点值之和(即根到该节点的路径和)。需要对镜像二叉树进行精简,规则如下:
规则一(剪枝):当某节点的镜像完整大小(路径和)$\leq 0$ 时,该节点异常,需要将该节点及其整棵子树从树中删除。
规则二(合并):当某个节点只有一个子节点时,该节点与子节点合并(值相加),合并后的节点继承子节点的左右子树。如果合并后仍是单子节点,继续合并。
说明:规则一优先于规则二执行。
输入为容器镜像二叉树的前序遍历数组和中序遍历数组,输出为精简后的后序遍历数组。
输入描述
第一行包含若干个整数,表示前序遍历结果。第二行包含若干个整数,表示中序遍历结果。各节点值互不相同,节点数 $n \leq 10000$。
输出描述
精简后的后序遍历输出。如果输出的二叉树为空,则输出字符串 null。
样例
输入
1 2 3 4 5 6
2 1 3 5 4 6
输出
2 5 6 7 1
输入
1 2 3 -5 5 6
2 1 3 5 -5 6
输出
2 3 1
输入
1 2 4 3 -9 6
2 1 4 3 -9 6
输出
2 7 1
思路分析
第一步:用前序 + 中序还原二叉树
前序遍历的首元素是当前子树的根。在中序中找到该根的位置 $k$,根左边的元素属于左子树,右边的属于右子树。预先用哈希表存「值 → 中序下标」的映射,每次 $O(1)$ 定位。
第二步:递归下行剪枝(规则一)
每层递归携带参数 \(\text{path\_sum}\),表示根到当前节点父亲的路径和。到达节点时计算它的镜像完整大小 \(\text{cur\_sum} = \text{path\_sum} + \text{node.val}\)。
若 \(\text{cur\_sum} \leq 0\),直接返回空,整棵子树被丢弃。注意判断的是路径和而非节点自身值——一个节点值为负,只要祖先累计够大也不会被剪。
第三步:递归回溯合并(规则二)
子树处理完后回到当前节点,检查它还剩几个孩子。若恰好只有一个孩子,则把孩子”吃掉”——值相加,左右指针替换为孩子的左右子树。合并后可能仍是单子节点,用 while 循环反复合并直到有 $0$ 或 $2$ 个孩子。
每合并一次节点数减 $1$,整棵树合并次数 $\leq n$,总开销线性。
第四步:后序输出
对精简后的树做标准后序遍历(左 → 右 → 根)。整棵树被剪光时输出 null。
题解代码
import sys
input = sys.stdin.readline
def solve():
pre_arr = list(map(int, input().split()))
in_arr = list(map(int, input().split()))
n = len(pre_arr)
in_pos = {v: i for i, v in enumerate(in_arr)}
def build(pl, pr, il, ir):
if pl > pr:
return None
root_val = pre_arr[pl]
k = in_pos[root_val]
left_size = k - il
left = build(pl + 1, pl + left_size, il, k - 1)
right = build(pl + left_size + 1, pr, k + 1, ir)
return [root_val, left, right]
def process(node, path_sum):
if node is None:
return None
cur_sum = path_sum + node[0]
if cur_sum <= 0:
return None
node[1] = process(node[1], cur_sum)
node[2] = process(node[2], cur_sum)
# 规则二:单子节点链反复合并
while (node[1] is None) != (node[2] is None):
child = node[1] if node[1] is not None else node[2]
node[0] += child[0]
node[1] = child[1]
node[2] = child[2]
return node
def postorder(node, result):
if node is None:
return
postorder(node[1], result)
postorder(node[2], result)
result.append(str(node[0]))
root = build(0, n - 1, 0, n - 1)
root = process(root, 0)
result = []
postorder(root, result)
if not result:
print("null")
else:
print(" ".join(result))
solve()
复杂度分析
时间复杂度:$O(n)$,重建、剪枝合并、后序遍历各一趟
空间复杂度:$O(n)$,哈希表 + 递归栈
第三题:技能树学习路径规划
题目描述
游戏中有一个技能树系统。每个技能都有学习所需的时间成本(小时)、提升的战斗力(各技能战斗力互不相同),以及前置技能(必须先学习的技能)。每个技能至多只有 $1$ 个直接前置技能,且依赖关系不会成环。
有有限的总学习时间 $T$,需要在技能树中挑选若干技能进行学习,使得在时间限制内获得最大总战斗力。
输入描述
第一行包含两个整数 $n, T$,分别表示技能数量和总可用时间。
接下来 $n$ 行,每行描述一个技能:技能编号、学习时间、提升的战斗力、前置技能数量($0$ 或 $1$);若有前置,行末再给出前置技能编号。
输出描述
输出一个整数,表示在时间 $T$ 内能获得的最大战斗力。如果无法完成任意一个技能,输出 $0$。
样例
输入
2 1
1 2 15 0
2 4 20 0
输出
0
两个技能的学习时间分别为 $2, 4$,均大于总时间 $1$,无法学习任何技能。
输入
4 8
1 3 25 0
2 4 30 0
3 5 40 1 1
4 2 20 1 2
输出
65
选学技能 $2$(时间 $4$)+ 技能 $4$(时间 $2$,前置为技能 $2$)+ 无法再选,等等——最优方案是选技能 $1$(时间 $3$)+ 技能 $3$(时间 $5$,前置为技能 $1$),总耗时 $8$,总战斗力 $25 + 40 = 65$。
思路分析
第一步:识别问题模型
每个技能至多 $1$ 个前置,依赖关系构成一片森林。选某个技能时,它到根的整条路径(所有前置)必须一起选——这是经典的树上依赖背包问题。
第二步:建虚拟根统一处理
加一个虚拟节点 $0$ 作为总根,把所有无前置的技能挂到节点 $0$ 上,森林变成一棵有根树。虚拟根不耗时也不加分。
第三步:状态定义与转移
定义 $f[u][j]$ = 子树 $u$ 中、必须选上 $u$ 本身、恰好花费 $j$ 小时时的最大战斗力。「必须选 $u$」把依赖约束写进了状态。
初始化:虚拟根 $f[0][0..T] = 0$(不耗时不加分,所有容量都可达);其他节点 \(f[u][\text{cost\_u}] = \text{val\_u}\)。
转移:按 DFS 后序逐个合并孩子。对于孩子 $v$,先构造 \(\text{best\_v}[y]\) = 子树 $v$ 花费 $\leq y$ 时的最大战斗力(前缀 max),然后做分组背包合并:
\[f'[u][j+y] = \max(f[u][j] + \text{best\_v}[y])\]枚举所有合法的 $(j, y)$ 对刷新答案。注意必须用新数组防止同一孩子被重复选取。
第四步:答案
$\max_{j=0}^{T} f[0][j]$ 即为最大战斗力。
题解代码
import sys
input = sys.stdin.readline
def solve():
data = sys.stdin.read().split()
idx = 0
n = int(data[idx]); idx += 1
T = int(data[idx]); idx += 1
cost = [0] * (n + 1)
val = [0] * (n + 1)
children = [[] for _ in range(n + 1)]
for _ in range(n):
sid = int(data[idx]); idx += 1
t = int(data[idx]); idx += 1
v = int(data[idx]); idx += 1
k = int(data[idx]); idx += 1
cost[sid] = t
val[sid] = v
parent = 0
if k == 1:
parent = int(data[idx]); idx += 1
children[parent].append(sid)
NEG = -1
f = [[NEG] * (T + 1) for _ in range(n + 1)]
# 迭代 DFS 避免递归栈溢出
order = []
stack = [0]
while stack:
u = stack.pop()
order.append(u)
for v in children[u]:
stack.append(v)
order.reverse() # 后序
for u in order:
if u == 0:
for j in range(T + 1):
f[u][j] = 0
else:
if cost[u] <= T:
f[u][cost[u]] = val[u]
for v in children[u]:
# best_v[y] = 子树 v 花费 <= y 的最大战斗力(不选 v 记 0)
best_v = [0] * (T + 1)
cur = 0
for y in range(T + 1):
if f[v][y] > cur:
cur = f[v][y]
best_v[y] = cur
nf = [NEG] * (T + 1)
for j in range(T + 1):
if f[u][j] == NEG:
continue
base = f[u][j]
limit = T - j
for y in range(limit + 1):
cand = base + best_v[y]
if cand > nf[j + y]:
nf[j + y] = cand
f[u] = nf
ans = 0
for j in range(T + 1):
if f[0][j] > ans:
ans = f[0][j]
print(ans)
solve()
复杂度分析
时间复杂度:$O(n \cdot T^2)$,树形背包的经典复杂度
空间复杂度:$O(n \cdot T)$
小结
- 第一题是经典二分答案模板,关键在于识别”答案单调 + check 可行”的模式。上界取 $\max(\text{tasks})$,check 函数累加向上取整的耗时即可
- 第二题是二叉树重建 + 多规则处理的综合题,核心是分清”规则一判路径和(自顶向下传参)、规则二判孩子数(自底向上合并)”的先后关系
- 第三题是树形依赖背包的标准形态,建虚拟根把森林统一、按后序合并孩子、始终从上一层读取避免重复选取,是该类型的三个关键 trick