大厂真题 / 华为
华为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 数组。
小结
- 激活重计算:端点决定分段代价,位置与预算共同入状态。
- 训练成本优化:先求最优最大负载,再在阈值下优化成本。