大厂真题 / 华为

华为 5.20 笔试真题 - 研发岗

本场考试概述

考试时间:2026年5月20日 考试岗位:研发岗(国内) 难度评级:中等偏难

考点分析

  1. 第一题:二分答案(难度中等)
  2. 第二题:二叉树前序+中序重建 + 路径和剪枝 + 单子节点链合并(难度中等偏难)
  3. 第三题:树形依赖背包(难度困难)

建议策略

  1. 第一题是二分答案模板题,识别”答案具有单调性 + 给定答案可以 check 可行性”的模式后快速切入,建议优先拿下
  2. 第二题先稳扎稳打把二叉树重建模板写出来,再逐步加上剪枝和合并逻辑;特别注意规则一判断的是”路径和”而非节点自身值
  3. 第三题是树形依赖背包,若没练过该类型可暂时跳过,先保前两题分数

第一题:服务器处理计算任务

题目描述

服务器集群中有 $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