大厂真题 / 阿里巴巴
阿里 5.16 笔试真题 - AI研发岗
本场考试概述
考试时间:2026年5月16日 考试岗位:AI研发岗 难度评级:中等
考点分析:
- 第一题:前缀和 + 组合计数(难度简单)
- 第二题:位运算观察题(难度简单)
- 第三题:差分扫描线 + 区间覆盖判定(难度困难)
建议策略:
- 第一题枚举中间字符
p,用前后缀计数把三层循环压成一次扫描 - 第二题快速识别”按位或不超过加法和”这一关键不等式,单元素贡献上界即 $a_i$ 自身,答案就是所有元素按位或
- 第三题把一次移动拆成”删除节省 + 插入开销”两部分,关键判据是”剩余数组里存在相邻对覆盖 $v$”,用差分扫描线 $O(n \log n)$ 解决
第一题:密钥密码
题目描述
给定一个只由字符 o、p、c 组成的字符串 $s$,长度为 $n$。
从 $s$ 中选择 3 个位置 $i < j < k$,满足:
- 第 $i$ 个字符必须是
o - 第 $j$ 个字符必须是
p - 第 $k$ 个字符可以是
c,也可以是o(因为有时会把o看成c)
把所有满足条件的方案数记为 $W$。如果不存在任何方案,则 $W = 0$。
输出 $W$ 对 $10^9 + 7$ 取模的结果。
样例
输入
3
4
oppc
3
opc
6
oooppc
输出
2
1
6
思路分析
第一步:暴力的瓶颈在哪里
直接枚举三元组 $(i, j, k)$ 是 $O(n^3)$,$n$ 最大 $10^5$ 级别必然超时。关键在于发现三个位置之间的约束只是”左中右顺序”——如果固定中间的 p,左右两侧互相独立,可以将乘法原理用上。
第二步:枚举中间字符拆成独立子问题
对每个位置 $j$(当 $s_j = $ p),它能形成的三元组数量 = 左侧 o 的数量 $\times$ 右侧合法字符的数量。合法字符是 o 或 c(即除 p 以外的字符)。
第三步:一次扫描同时维护前后缀计数器
定义两个变量:
- \(\text{cnt\_o}\):当前位置严格左侧已出现的
o数量 - \(\text{suf\_oc}\):当前位置严格右侧的
o和c总数
初始时 \(\text{suf\_oc}\) 等于整个字符串中 o 与 c 的总数。从左到右扫描:
- 先将当前位置从后缀中扣除(如果是
o或c则 \(\text{suf\_oc}\) 减 1),使其严格表示”右侧” - 如果当前字符是
p,把 \(\text{cnt\_o} \times \text{suf\_oc}\) 累加到答案 - 如果当前字符是
o,\(\text{cnt\_o}\) 加 1
这样只需一次线性扫描,每步 $O(1)$,整体 $O(n)$。
第四步:取模
\(\text{cnt\_o}\) 和 \(\text{suf\_oc}\) 均不超过 $n$,乘积最大约 $n^2 \approx 10^{10}$,64 位整数能装下,每次累加后对 $10^9+7$ 取模即可。
题解代码
import sys
input = sys.stdin.readline
MOD = 10**9 + 7
def solve(s):
# suf_oc 初始为整个串中 'o' 或 'c' 的总数
suf_oc = 0
for ch in s:
if ch == 'o' or ch == 'c':
suf_oc += 1
cnt_o = 0 # 当前下标左侧已出现的 'o' 数
ans = 0
for ch in s:
# 先把当前位从后缀剩余中扣掉,使 suf_oc 严格表示「右侧」合法 k 的数量
if ch == 'o' or ch == 'c':
suf_oc -= 1
# 当前是 'p' 才贡献方案:左侧 'o' 数 * 右侧合法 k 数
if ch == 'p':
ans = (ans + cnt_o * suf_oc) % MOD
if ch == 'o':
cnt_o += 1
return ans
T = int(input())
out = []
for _ in range(T):
input()
s = input().strip()
out.append(str(solve(s)))
print('\n'.join(out), end='')
复杂度分析
时间复杂度:$O(n)$,单组数据扫一遍字符串,多组合计 $O(\sum n)$ 空间复杂度:$O(1)$ 额外空间(只用两个计数器)
第二题:按位或最大化
题目描述
给定一个长度为 $n$ 的数组 $a$,初始时令 $x = 0$。
| 可以进行若干次如下操作(可以为 0 次):选择一个下标 $i$,再选择一个整数 $y$,满足 $1 \leq y \leq a_i$。令 $a_i = a_i - y$,同时令 $x = x \mathbin{ | } y$(按位或)。 |
目标是最大化最终得到的 $x$,输出这个最大值。
样例
输入
3
3
1 2 4
1
5
4
3 3 3 3
输出
7
5
3
思路分析
第一步:单个元素能给 $x$ 贡献多少
假设对 $a_i$ 操作了 $k$ 次,每次取出 $y_1, y_2, \ldots, y_k$。有两个约束:
- 总量约束:$y_1 + y_2 + \cdots + y_k \leq a_i$
-
按位或不超过加法和:对任意非负数,$y_1 \mathbin{ } y_2 \mathbin{ } \cdots \mathbin{ } y_k \leq y_1 + y_2 + \cdots + y_k$
原因很直观——按位或中重叠的位只算一次,而加法中重叠位会进位变成更高的 1,因此加法和总是 $\geq$ 按位或。
串联两个不等式:
\[x_i = y_1 \mathbin{|} \cdots \mathbin{|} y_k \leq y_1 + \cdots + y_k \leq a_i\]所以单个 $a_i$ 对 $x$ 的贡献上界就是 $a_i$ 自身。
第二步:这个上界达得到吗
达得到——直接一次操作取 $y = a_i$,$a_i$ 清零,$x$ 被或上完整的 $a_i$。单次贡献恰好等于 $a_i$。
第三步:多个元素叠加
不同下标的操作互相独立。把每个 $a_i$ 都”一次取完”,最终:
\[x = a_1 \mathbin{|} a_2 \mathbin{|} \cdots \mathbin{|} a_n\]所以答案就是所有元素的按位或。
题解代码
import sys
input = sys.stdin.readline
def solve(arr):
# 单元素能贡献给 x 的最大值即 a[i] 自身
# 多元素累积时,答案就是所有元素按位或
res = 0
for v in arr:
res |= v
return res
data = sys.stdin.buffer.read().split()
idx = 0
T = int(data[idx]); idx += 1
out = []
for _ in range(T):
n = int(data[idx]); idx += 1
a = [int(data[idx + j]) for j in range(n)]
idx += n
out.append(str(solve(a)))
print('\n'.join(out), end='')
复杂度分析
时间复杂度:$O(n)$,多组合计 $O(\sum n)$ 空间复杂度:$O(1)$ 额外空间
第三题:最小陡峭值
题目描述
定义一个数组的”陡峭值”为所有相邻元素的差的绝对值之和:
\[S = \sum_{i=1}^{n-1} |a_{i+1} - a_i|\]| 当数组长度为 1 时,陡峭值定义为 0。例如数组 $[2, 3, 1]$ 的陡峭值为 $ | 3-2 | + | 1-3 | = 3$。 |
现有一个长度为 $n$ 的数组 $a$,可以进行至多一次操作:从数组中取出一个元素(其他元素的相对顺序保持不变),再将该元素插入到数组的任意位置(含两端)。
求经过至多一次操作后能够得到的最小陡峭值。
样例
输入
3
3
2 3 1
4
3 1 4 2
1
1
输出
2
4
0
思路分析
第一步:把移动拆成”删除 + 插入”
一次操作等价于:先删除某个元素 $a_i$,再把它插回剩余数组的某个位置。新陡峭值 = 原始陡峭值 $S$ - 删除节省量 + 插入开销。目标是让”节省 - 开销”最大化。
第二步:分析删除的节省量
| 删除内部元素 $a_i$($0 < i < n-1$)时:原先的贡献是 $ | a_i - a_{i-1} | + | a_{i+1} - a_i | $,删除后左右邻居直接拼接产生 $ | a_{i+1} - a_{i-1} | $。节省量为: |
由三角不等式知 $\text{save} \geq 0$。对端点元素,节省量就是该端与唯一邻居的差值。
第三步:分析插入的开销
| 把值 $v = a_i$ 插入剩余数组某相邻对 $(l, r)$ 之间。原来这对贡献 $ | r - l | $,插入后变成 $ | v - l | + | r - v | $。插入开销为: |
关键观察:当 $\min(l, r) \leq v \leq \max(l, r)$ 时,由绝对值展开可知三项恰好抵消,$\text{cost} = 0$。也就是说,只要存在一对相邻元素”夹住” $v$,就可以零代价插入。
第四步:用差分扫描线快速判定”是否存在覆盖 $v$ 的相邻对”
每对相邻元素 $(a_k, a_{k+1})$ 对应数轴上的区间 $[\min(a_k, a_{k+1}),\ \max(a_k, a_{k+1})]$。问题变成:数轴上有 $n-1$ 个区间,查询点 $v$ 被多少个区间覆盖。
用差分 + 排序去重 + 二分查找:对每个区间 $[lo, hi]$,在 $lo$ 处 $+1$、$hi+1$ 处 $-1$,按坐标排序做前缀和。查询 $v$ 时二分找到最后一个 $\leq v$ 的坐标,其前缀和就是覆盖数。
第五步:修正移除 $a_i$ 对覆盖数的影响
移走 $a_i$ 后,剩余数组的相邻对 = 原来的 $n-1$ 对中删掉 $(a_{i-1}, a_i)$ 和 $(a_i, a_{i+1})$ 两对,再补上桥接对 $(a_{i-1}, a_{i+1})$。对覆盖计数做修正:
\[\text{cnt} = \text{cover}(v) - \text{covers}(\text{pair}_{i-1}, v) - \text{covers}(\text{pair}_i, v) + \text{covers}(\text{bridge}, v)\]若 $\text{cnt} \geq 1$,说明可以零代价插入,$\text{cost} = 0$。
第六步:极值元素的特判
当 $v$ 是数组的严格最大或严格最小值时,没有任何相邻对能跨过 $v$,此时 $\text{cnt} = 0$。此时需要暴力枚举所有有效相邻对计算最小插入开销。但这种情况最多出现常数次(全局最大值/最小值),所以总体仍然高效。
第七步:汇总答案
枚举每个 $i$ 的 $\text{base} - \text{save} + \text{ins}$,取全局最小值,并与不操作的 $S$ 比较。
题解代码
import sys
from bisect import bisect_right
def solve(n, a):
if n == 1:
return 0
if n == 2:
return abs(a[1] - a[0])
base = 0
plo = [0] * (n - 1)
phi = [0] * (n - 1)
for k in range(n - 1):
base += abs(a[k + 1] - a[k])
plo[k] = a[k] if a[k] < a[k + 1] else a[k + 1]
phi[k] = a[k] if a[k] > a[k + 1] else a[k + 1]
# 扫描线统计:每个值被多少个相邻对的区间 [plo, phi] 覆盖
events = {}
for k in range(n - 1):
events[plo[k]] = events.get(plo[k], 0) + 1
events[phi[k] + 1] = events.get(phi[k] + 1, 0) - 1
keys = sorted(events.keys())
cum = 0
cum_at = []
for k in keys:
cum += events[k]
cum_at.append(cum)
def count_cover(v):
idx = bisect_right(keys, v) - 1
if idx < 0:
return 0
return cum_at[idx]
def covers(lo, hi, v):
return 1 if lo <= v <= hi else 0
ans = base
for i in range(n):
v = a[i]
has_bridge = False
blo = bhi = 0
if i == 0:
save = abs(a[1] - a[0])
cnt = count_cover(v) - covers(plo[0], phi[0], v)
head_b, tail_b = a[1], a[n - 1]
elif i == n - 1:
save = abs(a[n - 1] - a[n - 2])
cnt = count_cover(v) - covers(plo[n - 2], phi[n - 2], v)
head_b, tail_b = a[0], a[n - 2]
else:
blo = a[i - 1] if a[i - 1] < a[i + 1] else a[i + 1]
bhi = a[i - 1] if a[i - 1] > a[i + 1] else a[i + 1]
has_bridge = True
save = abs(a[i] - a[i - 1]) + abs(a[i + 1] - a[i]) - abs(a[i + 1] - a[i - 1])
cnt = (count_cover(v)
- covers(plo[i - 1], phi[i - 1], v)
- covers(plo[i], phi[i], v)
+ covers(blo, bhi, v))
head_b, tail_b = a[0], a[n - 1]
if cnt >= 1:
ins = 0
else:
# v 是数组的严格极值,暴力枚举有效相邻对求最小插入开销
best = min(abs(v - head_b), abs(v - tail_b))
for k in range(n - 1):
if i > 0 and k == i - 1:
continue
if i < n - 1 and k == i:
continue
lo, hi = plo[k], phi[k]
cost = 0 if lo <= v <= hi else abs(v - lo) + abs(v - hi) - (hi - lo)
if cost < best:
best = cost
if has_bridge:
cost = 0 if blo <= v <= bhi else abs(v - blo) + abs(v - bhi) - (bhi - blo)
if cost < best:
best = cost
ins = best
cand = base - save + ins
if cand < ans:
ans = cand
return ans
data = sys.stdin.buffer.read().split()
idx = 0
T = int(data[idx]); idx += 1
out = []
for _ in range(T):
n = int(data[idx]); idx += 1
a = [int(data[idx + j]) for j in range(n)]
idx += n
out.append(str(solve(n, a)))
print('\n'.join(out), end='')
复杂度分析
时间复杂度:$O(n \log n)$,建差分前缀和需要排序 $O(n \log n)$,每次二分查询 $O(\log n)$,严格极值 fallback 最多触发常数次暴力 $O(n)$。多组合计 $O(\sum n \log n)$ 空间复杂度:$O(n)$,存所有相邻对的区间端点和差分前缀
小结
| 题号 | 考点 | 难度 | 核心思路 |
|---|---|---|---|
| T1 | 前缀和 + 组合计数 | 简单 | 固定中间 p,左侧 o 数 $\times$ 右侧合法字符数 |
| T2 | 位运算观察 | 简单 | OR $\leq$ SUM $\leq a_i$,上界可达,答案 = 全体 OR |
| T3 | 差分扫描线 + 区间覆盖 | 困难 | 移动 = 删除节省 + 插入开销;零代价插入等价于存在覆盖对 |
本场笔试前两题属于思维观察题,代码量极小,关键在于快速抓住性质。第三题是典型的”拆分代价 + 快速判定”模型:先用代数推导把插入代价归约为区间覆盖判定,再用差分扫描线将暴力 $O(n^2)$ 优化到 $O(n \log n)$。面对这类题目,建议先手推小样例确认公式正确性,再考虑数据结构加速。