大厂真题 / 华为

华为AI岗 2026-09-23

内容边界:材料明确标为“选择题节选 5 道”,原始正文实际披露第 1 至第 5 题,另有 2 道完整编程题。概述没有给出选择题总数,因此未披露题目的数量和编号未知,本文不补写其他题目、选项、约束或样例。

本场考试概述

考试时间:2026-09-23

考试岗位:AI岗

难度评级:中等偏难

考点分析:5 道选择题、检查点背包与前缀和、两阶段子集 DP。

建议策略:先稳住条件概率与基础概念题。编程第一题先写清端点是否收费,第二题严格区分最大负载和总成本的优化顺序。

选择题(实际披露 5 道)

1. 条件概率

已知 $P(A)=0.3$、$P(B)=0.5$、$P(B\mid A)=0.6$,求 $P(A\mid B)$。

  • A. 0.36
  • B. 0.5
  • C. 0.3
  • D. 0.6

答案:A。 $P(A\cap B)=0.3\cdot0.6=0.18$,因此 $P(A\mid B)=0.18/0.5=0.36$。A 正确;B 只是 $P(B)$;C 只是 $P(A)$;D 把条件方向相反的概率直接照搬。题目没有给出独立性,不能假设独立。

2. 线性激活函数

神经网络使用线性激活函数时,多个隐藏层的效果等价于什么?

  • A. 多个线性层
  • B. 一个线性变换
  • C. 没有隐藏层
  • D. 一个非线性变换

预期答案:B;术语及选项有歧义。

\[W_2(W_1x+b_1)+b_2=(W_2W_1)x+(W_2b_1+b_2).\]

A 只描述原有层数,没有表达可合并性;B 抓住了复合后仍可合并为一次映射的结论;D 错误,复合不会产生非线性。C 若意指直接删掉隐藏层及其作用,则不准确;若指“去掉隐藏层、保留一个适当的输出仿射层”,函数表达上也可以等价,不能无条件宣称这种理解错误。

严格说,含非零偏置时是仿射变换;只有无偏置或总偏置为零时才是线性变换。这里保留题目的四个选项,并明确这一措辞问题。

3. 稀疏矩阵存储

用户—物品评分矩阵的大部分元素为 $0$,以下哪种方式最节省空间?

  • A. 二维数组
  • B. 三元组列表(COO)
  • C. 连续内存块
  • D. 稠密矩阵存储

答案:B。 对 $m\times n$ 矩阵,A、D 都为所有元素保留位置,空间为 $O(mn)$;B 只存非零元素的行、列和值,空间为 $O(\mathrm{nnz})$,在题设高度稀疏时更省。C 只说明存储布局,不说明是否省略零元素,不能据此推出更省空间。此结论仅比较给出的选项,不声称 COO 比所有其他稀疏格式都省空间。

4. 词元化

关于 Tokenization 在数据预处理中的作用,哪项正确?

  • A. 词元化直接将文本转化为固定长度的向量。
  • B. BPE 是常见的字词分割算法,用于平衡词表大小与未登录词问题。
  • C. 词元化后,所有语言的每个字符都会被编码为一个单独词元。
  • D. 词元化会丢失所有标点符号信息。

答案:B。 A 错在混淆词元切分、编号与嵌入:词元化通常产生变长的词元序列;每个词元的固定维度嵌入也不等于整段文本自动变成固定长度。B 正确,BPE 反复合并高频相邻单元,用子词缓解未登录词问题,具体覆盖能力还取决于初始符号集和回退机制。C 错误,词元可以小于、等于或跨越字符;D 错误,标点通常保留,是否移除取决于额外的预处理规则。

5. 数据并行

标准数据并行(DP)中,哪项正确描述模型副本和数据分配?

  • A. 每个 GPU 存完整模型副本,处理不同数据批次。
  • B. 每个 GPU 存模型的一部分层,处理不同数据批次。
  • C. 每个 GPU 存完整模型副本,处理相同数据批次。
  • D. 每个 GPU 存模型的不同层,处理相同数据。

答案:A。 A 是复制模型并切分数据的标准做法,梯度同步后保持参数一致。B 将模型按层分割,更接近流水线并行,而非标准数据并行;C 让各卡重复处理同一批数据,不符合此处的数据分配策略;D 也是按层划分模型,不是完整副本。按层划分不应笼统等同于张量并行,后者通常切分层内张量计算;分片数据并行属于另有约定的扩展,本题讨论标准副本模式。


第 1 题:激活重计算

题目描述

模型有 $N$ 层,编号为 $1$ 到 $N$。第 $i$ 层的前向耗时为 $f_i$,保存该层激活值需要 $m_i$ 显存;它们分别对应输入数组下标 $i-1$。

第 $0$ 层是免费且常驻的起始检查点,第 $N$ 层是免费的终点。只能在中间层 $1$ 到 $N-1$ 选择额外检查点,总显存不超过 $B$。若相邻检查点为 $c<d$,其中第 $k$ 层会重算 $d-k$ 次,区间代价为

\[C(c,d)=\sum_{k=c+1}^{d-1} f_k\cdot(d-k).\]

相邻层之间的空区间代价为 $0$。求所有相邻检查点区间代价之和的最小值。

输入格式:第一行 $N,B$;第二行 $N$ 个前向耗时;第三行 $N$ 个显存开销。约束为 $1\le N\le100$、$0\le B\le1000$、$1\le f_i\le1000$、$1\le m_i\le100$。

输出格式:一个整数,表示最小重计算代价。第 $N$ 层虽有输入数据,但不占检查点显存,其前向耗时也不计入重计算区间。

样例 1

输入

4 5
10 20 30 40
3 4 2 6

输出

20

样例 2

输入

3 0
5 5 5
1 1 1

输出

15

思路分析

第一步:区间代价不能按单个检查点独立计算。 一个检查点是否划算,取决于它前后两个检查点的位置。仅做普通物品背包会丢失相邻关系,因此状态还要记录最后一个检查点。

第二步:用两个前缀和消掉区间枚举。 令 $P_x=\sum_{i=1}^x f_i$、$Q_x=\sum_{i=1}^x i\cdot f_i$。把 $d$ 从求和中提出,得到

\[C(c,d)=d\cdot(P_{d-1}-P_c)-(Q_{d-1}-Q_c).\]

这样每对端点只需常数时间计算,不必重新遍历区间。

第三步:按位置和显存做 DP。 令 $D[c][b]$ 表示最后一个检查点为 $c$、恰好使用 $b$ 显存时,从 $0$ 到 $c$ 的最小代价。初始化 $D[0][0]=0$,其他状态不可达。枚举下一个中间检查点 $d$,执行

\[D[d][b+m_d]=\min\bigl(D[d][b+m_d],D[c][b]+C(c,d)\bigr),\quad c<d,\ b+m_d\le B.\]

终点 $N$ 不收费,答案是 $\min_{c,b}(D[c][b]+C(c,N))$。不选择任何中间检查点的路径始终存在,因此 $B=0$ 也能处理;$N=1$ 的答案为 $0$。

样例推演:样例 1 选第 $1,3$ 层,显存 $3+2=5$,只有区间 $(1,3)$ 重算第 $2$ 层,代价 $20$。样例 2 没有额外检查点,代价 $5\cdot2+5\cdot1=15$。

正确性证明

对检查点位置归纳。任意以 $d$ 结尾的方案都有唯一的前一检查点 $c$;删去末段后,它必对应某个 $D[c][b]$。该末段代价恰为 $C(c,d)$,新增显存恰为 $m_d$,所以转移覆盖所有合法方案。反之每个转移都连接递增端点且遵守预算,不会产生非法方案。最后连接免费的终点并对全部显存状态取最小,即得到全局最优值。

实现说明:使用标准库即可表达该规模的 DP;没有题目时限证据时,不能断言纯 Python 必然超时,也不能把 NumPy 当作提交环境的必备依赖。

题解代码

import sys
input = sys.stdin.readline


def min_cost(n, budget, forward_time, memory):
    prefix = [0] * (n + 1)
    weighted = [0] * (n + 1)
    for i, value in enumerate(forward_time, 1):
        prefix[i] = prefix[i - 1] + value
        weighted[i] = weighted[i - 1] + i * value
    inf = float('inf')
    dp = [[inf] * (budget + 1) for _ in range(n)]
    dp[0][0] = 0
    answer = inf
    for end in range(1, n + 1):
        need = memory[end - 1] if end < n else 0
        if need > budget:
            continue
        for start in range(end):
            cost = end * (prefix[end - 1] - prefix[start])
            cost -= weighted[end - 1] - weighted[start]
            previous = dp[start]
            if end == n:
                answer = min(answer, min(previous) + cost)
            else:
                current = dp[end]
                for used in range(budget - need + 1):
                    candidate = previous[used] + cost
                    if candidate < current[used + need]:
                        current[used + need] = candidate
    return answer


def solve():
    n, budget = map(int, input().split())
    forward_time = list(map(int, input().split()))
    memory = list(map(int, input().split()))
    print(min_cost(n, budget, forward_time, memory))


solve()

复杂度分析

时间复杂度:$O(N^2(B+1))$,包括零预算情形。

空间复杂度:$O(N(B+1))$,保存 DP 表与前缀和。


第 2 题:训练成本优化

题目描述

将 $n$ 个梯度计算任务分配给 $k$ 张 NPU,每个任务恰好分配一次。先最小化所有卡的最大总耗时 $T$;若有多个方案达到同一个最优 $T$,再最小化总训练成本。

设第 $i$ 张卡的任务总耗时为 $s_i$,基础学习率为 $b$,衰减系数为 $r$,则

\[\mathrm{lr}_i=b\cdot r^{T-s_i},\qquad \mathrm{Cost}=\sum_{i=1}^{k}s_i\cdot b\cdot r^{T-s_i}.\]

输入格式:一行四段,以分号分隔:任务耗时列表;NPU数量;基础学习率;衰减系数。列表中的数以逗号分隔。约束为 $1\le n\le12$、$0\le t_i\le1000$、$1\le k\le n$、$0<b\le1$、$0<r<1$;两个小数均不超过四位小数。

输出格式:最小最大总耗时;总训练成本,成本保留四位小数。

格式说明纠正:材料的格式说明另写了 6;1.6590,但它不是下面第一个输入的正确结果。该输入总耗时为 $20$,三张卡的最大负载至少为 $\lceil20/3\rceil=7$;应以正式样例的 7;1.9400 为准。

样例 1

输入

4,3,2,3,5,2,1;3;0.1;0.9

输出

7;1.9400

样例 2

输入

5;1;0.1;0.9

输出

5;0.5000

思路分析

第一步:分清两个目标的优先级。 不能把最大耗时和成本加权成一个数,也不能只在某一个最小最大耗时方案上计算成本。先求出全局最优阈值 $T$,再在这个阈值内重新优化成本。

第二步:用任务集合消除重复计算。 $n\le12$,只有 $2^n$ 个子集。以位掩码 $M$ 表示任务集合,预处理 $S[M]$ 为其耗时之和。直接给每个任务枚举卡号会产生 $k^n$ 种分配,子集 DP 则复用相同的剩余任务状态。

第三步:第一遍 DP 求负载。 $G_j[M]$ 表示把 $M$ 分成 $j$ 个非空任务组的最小最大负载。初值 $G_0[0]=0$,其余为无穷大。枚举本组 $U\subseteq M$:

\[G_j[M]=\min_U\max\bigl(G_{j-1}[M\setminus U],S[U]\bigr).\]

只枚举含 $M$ 最低位任务的 $U$,因为任一无标号划分恰有一组包含该任务;把这一组作为当前组不会漏解。组数不足、或剩余任务不够填满其余卡的状态直接跳过。

第四步:固定 $T=G_k[\mathrm{full}]$,重新优化成本。 令 $h(s)=s\cdot r^{T-s}$。第二遍使用同样的子集枚举,但只允许 $S[U]\le T$,并把取最大值换成求和:

\[F_j[M]=\min_{U\subseteq M,\ S[U]\le T}\bigl(F_{j-1}[M\setminus U]+h(S[U])\bigr).\]

最终成本为 $b\cdot F_k[\mathrm{full}]$。每一遍只保留上一层和当前层。任务耗时可以为零,非空任务组不等于正负载;所有任务都是零时输出 0;0.0000。

正确性证明

两次转移都将一个划分唯一拆成“包含最低位任务的一组”和其余组,因此完整枚举全部集合划分。第一遍的组合运算是各组负载的最大值,故得到最优 $T$。第二遍只排除超过 $T$ 的组,保留所有一级目标最优方案;总成本可逐组相加,故再次取最小得到二级目标最优值。

即使允许某些卡空闲,也存在使用 $k$ 个非空任务组的最优方案:固定 $T$ 时,对 $a,b\ge0$,有 $h(a)+h(b)\le h(a+b)$,因为 $r^{T-a},r^{T-b}\le r^{T-a-b}$。拆组不增大最大负载,也不增加成本;又因 $k\le n$,可不断拆到 $k$ 组。

样例 1 的负载可为 $7,7,6$,成本为 $0.7+0.7+0.54=1.94$;样例 2 只有一组,成本为 $5\cdot0.1=0.5$。最后才格式化小数,不在转移中反复舍入。

题解代码

import sys
input = sys.stdin.readline


def optimize(times, k, base_lr, decay):
    n = len(times)
    size = 1 << n
    subset_sum = [0] * size
    count = [0] * size
    for mask in range(1, size):
        low = mask & -mask
        subset_sum[mask] = subset_sum[mask ^ low] + times[low.bit_length() - 1]
        count[mask] = count[mask ^ low] + 1

    inf = float('inf')
    previous = [inf] * size
    previous[0] = 0
    for groups in range(1, k + 1):
        current = [inf] * size
        for mask in range(1, size):
            if not groups <= count[mask] <= n - k + groups:
                continue
            low = mask & -mask
            rest = mask ^ low
            sub = rest
            while True:
                group = sub | low
                candidate = max(previous[mask ^ group], subset_sum[group])
                if candidate < current[mask]:
                    current[mask] = candidate
                if sub == 0:
                    break
                sub = (sub - 1) & rest
        previous = current
    best_time = previous[-1]

    cost = [inf] * size
    for mask, load in enumerate(subset_sum):
        if load <= best_time:
            cost[mask] = load * decay ** (best_time - load)
    previous = [inf] * size
    previous[0] = 0.0
    for groups in range(1, k + 1):
        current = [inf] * size
        for mask in range(1, size):
            if not groups <= count[mask] <= n - k + groups:
                continue
            low = mask & -mask
            rest = mask ^ low
            sub = rest
            while True:
                group = sub | low
                candidate = previous[mask ^ group] + cost[group]
                if candidate < current[mask]:
                    current[mask] = candidate
                if sub == 0:
                    break
                sub = (sub - 1) & rest
        previous = current
    return best_time, previous[-1] * base_lr


def solve():
    raw_times, raw_k, raw_lr, raw_decay = input().strip().split(';')
    times = list(map(int, raw_times.split(',')))
    best_time, cost = optimize(times, int(raw_k), float(raw_lr), float(raw_decay))
    print(f'{best_time};{cost:.4f}')


solve()

复杂度分析

时间复杂度:$O(k\cdot3^n)$,两遍子集 DP;所有掩码的子集数之和为 $3^n$。

空间复杂度:$O(2^n)$,子集和、成本表和滚动 DP 数组。


小结

  • 激活重计算:端点决定分段代价,位置与预算共同入状态。
  • 训练成本优化:先求最优最大负载,再在阈值下优化成本。