大厂真题 / 阿里巴巴
阿里 4.1 笔试真题 - 研发岗
本场考试概述
考试时间:2026年4月1日 考试岗位:研发岗 难度评级:中等偏难
考点分析:
- 第一题:贪心(难度简单)
- 第二题:差值约束 + BFS建图(难度中等)
- 第三题:前缀和 + 平衡计数(难度困难)
建议策略:
- 第一题贪心思路直观,先拿满分稳住心态
- 第二题关键在于把差值约束建成带权图,想清楚边权含义是核心
- 第三题需要推导中位数条件,将问题转化为前缀和配对,有一定推导难度
第一题:数组对齐
题目描述
给定两个长度均为 n 的非负整数数组 a 与 b。可以反复执行以下三种操作(每次操作使所选元素的值减 1;若元素当前为 0 则不允许执行):
- 操作1:选择一个下标 i,令 a[i] -= 1。
- 操作2:选择一个下标 i,令 b[i] -= 1。
- 操作3:选择两个下标 i, j(可以相同),同时令 a[i] -= 1 且 b[j] -= 1。
通过若干次操作,使得最终对所有 i 都满足 a[i] == b[i]。求最少操作次数。
样例
输入
2
4
1 2 3 4
2 1 3 5
3
0 0 5
2 1 0
输出
2
5
题解:贪心
| 每个位置 i 只能做减法,所以 a[i] 和 b[i] 最终一定等于 min(a[i], b[i])。设 d[i] = a[i] - b[i],若 d[i] > 0 说明 a 侧需要减 d[i],若 d[i] < 0 说明 b 侧需要减 | d[i] | 。 |
| 设 sum_a = 所有 d[i] > 0 的 d[i] 之和,sum_b = 所有 d[i] < 0 的 | d[i] | 之和。操作3 可以同时消耗 a 侧 1 点和 b 侧 1 点,贪心策略是尽量多用操作3 来把两侧消耗并行处理,最多使用 min(sum_a, sum_b) 次,剩余差额只能用操作1 或操作2 逐一消耗。因此答案为 max(sum_a, sum_b)。 |
时间复杂度:O(n)。 空间复杂度:O(1)。
import sys
input = sys.stdin.readline
def solve():
n = int(input())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
sum_a = sum_b = 0
for i in range(n):
d = a[i] - b[i]
if d > 0:
sum_a += d
else:
sum_b += -d
print(max(sum_a, sum_b))
t = int(input())
for _ in range(t):
solve()
第二题:约束差值数组
题目描述
构造一个长度为 n 的正整数数组,其中每个元素在 [1, 10^18] 范围内。给定 m 条约束,每条约束给出三元组 (u, v, k),要求 a[u] - a[v] = k。判断是否存在满足所有约束的数组。若存在则输出任意一个符合条件的数组;否则输出 -1。
样例
输入
2
3 2
1 2 1
2 3 1
2 2
1 2 100
2 1 1
输出
3 2 1
-1
题解:BFS建图
约束 a[u] - a[v] = k 把变量之间建立了严格的线性关系。在同一连通分量内,只要确定一个变量的值,其余所有变量的值都被唯一确定。
建图:每条约束 (u, v, k) 建两条有向边:u -> v 权重 -k,v -> u 权重 k。边权含义是”从已知节点推算邻居时需要加上的偏移量”。
BFS 赋值:对每个未赋值节点出发做 BFS,起点临时赋值 0,沿边推导邻居的值。若到达已赋值节点时发现新旧值矛盾,则无解输出 -1。
平移为正整数:BFS 得到的值可能有负数或零。找到连通分量内最小值 mn,将所有值加上 (1 - mn) 使最小值恰好为 1,再检查最大值是否超过 10^18。
时间复杂度:O(n + m)。 空间复杂度:O(n + m)。
import sys
from collections import deque
input = sys.stdin.readline
def solve():
n, m = map(int, input().split())
adj = [[] for _ in range(n + 1)]
for _ in range(m):
u, v, k = map(int, input().split())
adj[u].append((v, -k))
adj[v].append((u, k))
val = [None] * (n + 1)
ok = True
for s in range(1, n + 1):
if val[s] is not None:
continue
val[s] = 0
q = deque([s])
comp = [s]
while q and ok:
u = q.popleft()
for v, w in adj[u]:
if val[v] is None:
val[v] = val[u] + w
q.append(v)
comp.append(v)
elif val[v] != val[u] + w:
ok = False
break
if not ok:
break
mn = min(val[x] for x in comp)
shift = 1 - mn
for x in comp:
val[x] += shift
mx = max(val[x] for x in comp)
if mx > 10**18:
ok = False
break
if not ok:
print(-1)
else:
print(" ".join(str(val[i]) for i in range(1, n + 1)))
t = int(input())
for _ in range(t):
solve()
第三题:中,太中了
题目描述
给定一个长度为 n 的排列 a,对每个位置 i,统计元素 a[i] 在多少个连续子区间中恰好是该子区间的中位数。
中位数定义:对于长度为 len 的数组,将所有元素从小到大排序后,位于第 ceil(len/2) 个位置的元素即为中位数。
样例
输入
2
3
1 2 3
3
2 1 3
输出
1 3 2
3 1 2
题解:前缀和 + 平衡计数
暴力枚举所有子区间再排序是 O(n^3 log n),远超时限。核心观察:判断 a[i] 是否为子区间的中位数,不需要排序,只要看区间里比 a[i] 大的和比 a[i] 小的元素个数的关系。
中位数条件推导:设区间 [l, r] 中比 a[i] 小的有 s 个、比 a[i] 大的有 g 个。a[i] 是中位数等价于 g == s(奇数长度子区间)或 g == s + 1(偶数长度子区间)。给区间内除 a[i] 外的元素打标记:大于记 +1,小于记 -1,则标记总和 balance = g - s。中位数条件变为 balance == 0 或 balance == 1。
前缀和配对:包含位置 i 的子区间的标记和可拆为左半前缀和与右半前缀和。先向左扫描把每种前缀和值的出现次数存入计数数组,再向右扫描对每个 pre_right 查询配对的 -pre_right 和 -pre_right - 1 对应的个数,累加到答案。
时间复杂度:O(n^2)。 空间复杂度:O(n)。
import sys
input = sys.stdin.readline
def solve():
n = int(input())
a = list(map(int, input().split()))
ans = [0] * n
offset = n + 2
sz = 2 * n + 5
for i in range(n):
cnt = [0] * sz
cnt[0 + offset] = 1
cur = 0
# 向左扩展,统计左侧前缀和频次
for j in range(i - 1, -1, -1):
cur += 1 if a[j] > a[i] else -1
cnt[cur + offset] += 1
# 向右扩展并匹配
res = 0
cur = 0
for r in range(i, n):
if r > i:
cur += 1 if a[r] > a[i] else -1
res += cnt[-cur + offset]
res += cnt[-cur - 1 + offset]
ans[i] = res
print(" ".join(map(str, ans)))
t = int(input())
for _ in range(t):
solve()
小结
- 第一题贪心,核心是观察到操作3可以并行消耗两侧差值,答案即为两侧差值总和的较大者
- 第二题差值约束转化为带权图 BFS,逐连通分量赋值后平移至正整数范围,注意矛盾检测
- 第三题前缀和+平衡计数是中位数类问题的经典技巧,将”排序后第 k 个”转化为大小关系计数,再利用前缀和配对高效统计