大厂真题 / 拼多多
拼多多AI Infra 研发岗 2026-10-11
内容边界:正文完整披露 4 道编程题,以下保留全部题意、约束和正式样例。岗位明确为 AI Infra 研发方向。
本场考试概述
考试时间:2026-10-11
考试岗位:AI Infra 研发岗
难度评级:中等偏难
考点分析:前缀和、滑动窗口与堆、虚拟节点最小生成树、欧拉回路与重构树。
建议策略:先抓住相邻交换只影响一个前缀的性质,再处理窗口最值。后两题都需要先建模:建中心变为虚拟边,巡检补路变为奇度点配对。
第 1 题:冰柜指令看反后仍安全的位置数
题目描述
冰柜容量为 $C$,初始冰量为 $h$,依次执行 $n$ 条整数指令 $a_i$,每条执行后冰量增加 $a_i$。全过程安全指每一步之后的冰量都位于闭区间 $[0,C]$。
必须选一个下标 $1\le i<n$,交换第 $i$ 条与第 $i+1$ 条指令,然后执行完整序列。求使全过程安全的交换下标个数。即使两条指令数值相同,也算一个交换位置;不能把不交换算作一种方案;$n=1$ 时答案为零。
输入格式:仅一组数据。首行 $n,C,h$,其中 $1\le n\le2\times10^5$,$0\le h\le C\le10^9$;第二行 $n$ 个整数,$-10^9\le a_i\le10^9$。前缀和可能超出 32 位有符号整数范围。
输出格式:一个整数,表示合法交换位置数。
样例
输入
5 10 5
4 -8 6 2 -3
输出
2
输入
4 5 2
3 2 -4 1
输出
1
思路分析
第一步:缩小交换的影响范围。 令 $P_0=h$,$P_j=h+\sum_{t=1}^j a_t$。交换后,在第 $i$ 步之前没有变化,第 $i+1$ 步之后累计加入的数也没有变化。唯一变化的是
\[P'_i=P_{i-1}+a_{i+1}.\]若对每个交换位置重新模拟,需要 $O(n^2)$;现在只要检查一个新值,并保证其他旧前缀都合法。
第二步:数原本越界的前缀。 设共有 $b$ 个原前缀不在 $[0,C]$ 中。对候选位置 $i$,删去原前缀 $P_i$ 后仍越界的数量为 $b-[P_i\text{ 越界}]$。只有该数量为零且 $P’_i$ 合法时才计数。因此两处以上越界一定无解;唯一越界若在 $P_n$,也无解,因为总和不受交换影响。
第三步:检查边界。 循环仅枚举 $1$ 到 $n-1$,自然覆盖 $n=1$。冰量恰为零或容量都合法。第一组样例可交换的位置为 $i=3,4$;第二组只有 $i=2$。Python 整数自动支持本题所需的大整数。
题解代码
import sys
input = sys.stdin.readline
def count_safe_swaps(n, capacity, initial, a):
prefix = [initial]
for value in a:
prefix.append(prefix[-1] + value)
bad = sum(not 0 <= value <= capacity for value in prefix[1:])
answer = 0
for i in range(1, n):
remaining_bad = bad - (not 0 <= prefix[i] <= capacity)
if remaining_bad == 0 and 0 <= prefix[i - 1] + a[i] <= capacity:
answer += 1
return answer
def solve():
n, capacity, initial = map(int, input().split())
a = list(map(int, input().split()))
print(count_safe_swaps(n, capacity, initial, a))
solve()
复杂度分析
时间复杂度:$O(n)$,两次线性扫描。
空间复杂度:$O(n)$,保存指令和前缀和。
第 2 题:团子体重可能不变的最长连续日子
题目描述
按日期给出 $n$ 条整数闭区间 $[l_i,r_i]$,代表当天体重的可能值。选择非空连续日子 $[L,R]$,可以忽略至多一天的记录,但忽略后至少保留一天,并且剩下所有区间应包含某个共同整数 $x$。
求所选连续日子的最大长度 $R-L+1$,被忽略的那一天仍计入长度。
输入格式:仅一组数据。首行 $1\le n\le2\times10^5$;随后 $n$ 行各有 $l_i,r_i$,满足 $1\le l_i\le r_i\le10^9$。
输出格式:一个整数,表示最长合法连续段长度。
样例
输入
5
1 5
2 4
8 9
3 6
4 7
输出
5
思路分析
第一步:把交集条件化为最值。 不删除记录时,公共整数存在当且仅当最大左端点不超过最小右端点;由于端点是整数,无须再考虑取整。
设最大左端点来自下标 $i$,最小右端点来自下标 $j$。删除其他任何一天都不会改变这两个冲突的最值,所以只需考虑删除 $i$ 或 $j$。若 $i=j$,该区间被所有其他区间包含,本来就有交集。
第二步:保留两个极值,且不能去重。 当 $i\ne j$ 时,删除 $i$ 后最小右端点不变,只需检查第二大左端点是否不超过最小右端点;删除 $j$ 同理,只需检查最大左端点是否不超过第二小右端点。相同端点来自不同日期时必须分别保存,不能误把第二大理解成第二个不同的数值。
第三步:滑动窗口维护。 合法窗口的任意非空子窗口仍然合法:沿用原来删掉的那一天即可;若子窗口只剩一天,就不删。因此固定右端时,合法左端形成一个后缀,双指针只需单调收缩左端。
代码采用两个懒删除堆:一个存 $(-l_i,i)$,另一个存 $(r_i,i)$。删除堆顶中下标小于左端的旧记录;临时弹出当前最值,清理过期元素后读取第二个极值,再放回。窗口长度至少为二时第二个极值一定存在;长度一直接合法。堆中未到顶的过期元素可暂时保留,总空间仍为 $O(n)$。这种写法牺牲单调队列方案的线性时间,换取更直接的“前两名”维护,仍适用于给定规模。
样例忽略第 3 天后,其余区间都包含整数 $4$,所以答案为 $5$。
题解代码
import sys
import heapq
input = sys.stdin.readline
def top_two(heap, lo):
while heap[0][1] < lo:
heapq.heappop(heap)
first = heapq.heappop(heap)
while heap and heap[0][1] < lo:
heapq.heappop(heap)
second = heap[0][0] if heap else float("inf")
heapq.heappush(heap, first)
return first, second
def longest(intervals):
max_left = []
min_right = []
lo = 0
answer = 0
for hi, (left, right) in enumerate(intervals):
heapq.heappush(max_left, (-left, hi))
heapq.heappush(min_right, (right, hi))
while lo < hi:
(negative_left, i), negative_second = top_two(max_left, lo)
(small_right, j), second_right = top_two(min_right, lo)
if i == j or -negative_second <= small_right or -negative_left <= second_right:
break
lo += 1
answer = max(answer, hi - lo + 1)
return answer
def solve():
n = int(input())
intervals = [tuple(map(int, input().split())) for _ in range(n)]
print(longest(intervals))
solve()
复杂度分析
时间复杂度:$O(n\log n)$,每次判定做常数次堆操作,左右端点总移动次数为 $O(n)$。
空间复杂度:$O(n)$,两个堆和区间数组。
第 3 题:冷链物流网络最小总代价
题目描述
有 $N$ 个城市和 $M$ 条已有双向公路。可以花费 $W$ 开通某条公路作为冷链干线,也可以在城市 $i$ 花费 $C_i$ 建物流集散中心。所有建有中心的城市通过航空通道直接互通。求使所有城市连通的最小总代价。
必须至少建一个中心,即使 $N=1$ 也如此。
输入格式:首行 $N,M$,$1\le N\le10^5$,$0\le M\le2\times10^5$;第二行给出 $N$ 个中心费用,$1\le C_i\le10^9$;接下来 $M$ 行各给 $u,v,W$,$1\le u,v\le N$,$u\ne v$,$1\le W\le10^9$,没有重边。
输出格式:一个整数,表示最小总代价。输入结构和样例均为单组数据,没有额外的测试组数。
样例
输入
4 3
50 50 50 10
1 2 10
2 3 10
3 4 100
输出
80
思路分析
第一步:为所有中心引入共同连接点。 新建虚拟城市 $0$,在它与每个城市 $i$ 之间添加权值为 $C_i$ 的边。选这条边就表示在 $i$ 建中心,所有中心经点 $0$ 互通。
第二步:证明等价。 原题任意合法方案都至少有一个中心,将对应虚拟边加入后,$N+1$ 个点连通,费用不变;反过来,扩展图的任何连通方案都包含至少一条与点 $0$ 相连的边,因此能还原出至少一个中心,且所有城市互通。由于边权为正,最优连通方案一定可以删掉环而不增加费用,所以求扩展图的最小生成树即可。
第三步:Kruskal 选边。 将 $N+M$ 条边按费用排序,并查集维护连通块。只有两端分属不同块才选边;采用按大小合并和路径压缩。扩展图共有 $N+1$ 个点,选出 $N$ 条边即可停止。虚拟边保证整个图连通,原公路不连通或没有公路都不影响有解。
样例选择城市 1 和城市 4 的中心,再开通 $1!\leftrightarrow!2$、$2!\leftrightarrow!3$,代价为 $50+10+10+10=80$。单城时直接支付该城中心费用。
题解代码
import sys
input = sys.stdin.readline
def min_total_cost(n, cost, roads):
edges = [(weight, 0, i + 1) for i, weight in enumerate(cost)]
edges.extend((weight, u, v) for u, v, weight in roads)
edges.sort()
parent = list(range(n + 1))
size = [1] * (n + 1)
def find(x):
while x != parent[x]:
parent[x] = parent[parent[x]]
x = parent[x]
return x
total = picked = 0
for weight, u, v in edges:
u, v = find(u), find(v)
if u == v:
continue
if size[u] < size[v]:
u, v = v, u
parent[v] = u
size[u] += size[v]
total += weight
picked += 1
if picked == n:
break
return total
def solve():
n, m = map(int, input().split())
cost = list(map(int, input().split()))
roads = [tuple(map(int, input().split())) for _ in range(m)]
print(min_total_cost(n, cost, roads))
solve()
复杂度分析
时间复杂度:$O((N+M)\log(N+M))$,排序占主导。
空间复杂度:$O(N+M)$。
第 4 题:环线巡游巡检全部道路的最小花费
题目描述
连通无向图有 $n$ 个景点,编号 $1$ 到 $n$,有 $m$ 条道路,按输入顺序编号 $1$ 到 $m$。道路 $i$ 连接 $u_i,v_i$,过路费为 $w_i$,允许自环和重边。从景点 1 出发,可以反复做两类操作:
- 步行沿一条相邻道路到另一端,支付该边过路费,并巡检这条道路。每条道路至少要这样巡检一次。
- 乘快线到任意不同景点。自选一条连接两点的路线,允许重复经过景点和道路。若该路线上出现的最大道路编号为 $e$,本次票价仅为 $w_e$,并非最大边权,也不是路线边权之和。快线不算步行巡检。
求巡检全部道路、最后回到景点 1 的最小花费。
输入格式:首行 $1\le T\le10^4$。每组先读 $n,m$,$1\le n\le2\times10^5$,$0\le m\le2\times10^5$;随后 $m$ 行按道路编号给出 $u_i,v_i,w_i$,$1\le u_i,v_i\le n$,$1\le w_i\le10^9$。每组图连通,所有组的 $n$ 之和及 $m$ 之和均不超过 $5\times10^5$。
输出格式:每组一行一个最小花费。
样例
输入
1
4 4
1 2 10
2 3 10
3 4 10
1 1 1
输出
32
思路分析
第一步:先支付不可避免的巡检费用。 每条边至少步行一次,基础费用为 $\sum_i w_i$。为了最后回到起点,所有经过的步行边和快线移动组成的连通多重图必须全部为偶度数,也就是存在欧拉回路。原图奇度点必须由额外移动补成偶数,自环贡献两个度数。
额外移动可以分解成连接奇度点的路径与环,正费用环可以省去。一条额外步行路径的费用不小于这条路线上最大编号边的费用,改乘同路线快线不会更贵。快线还满足下面的超度量性质,因此多段快线也可以合成一次而不增加费用。最终只需求奇度点的最小费用两两配对。
第二步:按编号建重构树,不能按边权排序。 从仅含叶子 $1,\ldots,n$ 开始,按道路编号递增加边。如果合并两个不同连通块,就建立一个父节点,两个块的树根为孩子;否则不建节点。每条边的权值记到它加入后所属连通块的树节点上,节点的 $own$ 是挂在这里的所有边权的最小值。自环和成环边虽不合并,却仍然可能显著降低票价,不能丢弃。
对节点 $v$ 定义
\[price(v)=\min\bigl(own(v),price(parent(v))\bigr).\]根的价格就是自己的 $own$。没有挂边时 $own$ 视为正无穷。
第三步:证明票价公式。 若路线最大编号为 $e$,则在加入前 $e$ 条边的图中,两个端点和边 $e$ 属于同一连通块。反过来,只要在同一块,就能先走到边 $e$、经过它、再到终点;允许绕路和重复使该构造合法。因此所有可能用作最大编号边的候选,正是两点最近公共祖先及其祖先节点上挂的边。于是
\[cost(x,z)=price(\operatorname{LCA}(x,z)).\]沿树向上 $price$ 单调不减,故 $cost(p,q)\le\max(cost(p,r),cost(r,q))$。这也说明合并额外移动不会更贵。
第四步:从下往上配对。 每个叶子若为奇度点,就向上交一个待配点。两个孩子各交一个时,在当前节点配成一对,支付 $price$;否则将剩余的一个向上交。
交换论证:若子树内两点 $p,q$ 分别与外部 $p’,q’$ 配对,把它们改为内部一对、外部一对。内部价格不超过原两对中较小的价格,外部一对由超度量性质不超过原两对中较大的价格,总价不增。因此每个子树最多留一个点向外配是最优的。奇度点总数为偶数,根不会剩点。
父节点编号总大于孩子,因此倒序传播价格、正序合并奇偶即可,无须递归或真正查询 LCA。并查集另用按大小合并,避免重构树很深时影响查询效率。
样例先巡检 1、2、3 号边到达景点 4,花费 30;快线沿 3、2、1、4 号边回到 1,最大编号为 4,票价仅 1;再步行巡检 4 号自环,花费 1,总计 32。$n=1$ 时没有奇度点,答案就是自环费用之和;$m=0$ 的连通图只能是单点,答案为零。
题解代码
import sys
input = sys.stdin.readline
def minimum_tour(n, edges):
limit = 2 * n + 1
infinity = 10 ** 30
parent = [0] * limit
left = [0] * limit
right = [0] * limit
price = [infinity] * limit
odd = [0] * limit
dsu = list(range(n + 1))
size = [1] * (n + 1)
node_of = list(range(n + 1))
def find(x):
while dsu[x] != x:
dsu[x] = dsu[dsu[x]]
x = dsu[x]
return x
count = n
total = 0
for u, v, weight in edges:
total += weight
odd[u] ^= 1
odd[v] ^= 1
a, b = find(u), find(v)
if a == b:
node = node_of[a]
price[node] = min(price[node], weight)
else:
count += 1
x, y = node_of[a], node_of[b]
left[count], right[count] = x, y
parent[x] = parent[y] = count
price[count] = weight
if size[a] < size[b]:
a, b = b, a
dsu[b] = a
size[a] += size[b]
node_of[a] = count
for node in range(count - 1, 0, -1):
if parent[node]:
price[node] = min(price[node], price[parent[node]])
for node in range(n + 1, count + 1):
a, b = left[node], right[node]
if odd[a] and odd[b]:
total += price[node]
odd[node] = odd[a] ^ odd[b]
return total
def solve():
tests = int(input())
answers = []
for _ in range(tests):
n, m = map(int, input().split())
edges = [tuple(map(int, input().split())) for _ in range(m)]
answers.append(str(minimum_tour(n, edges)))
print("\n".join(answers))
solve()
复杂度分析
时间复杂度:$O((n+m)\alpha(n))$,按编号读入即为处理顺序,树上两遍扫描均为 $O(n)$。
空间复杂度:$O(n+m)$,重构树不超过 $2n-1$ 个节点,另保存输入边。
小结
- 相邻交换后只有一个前缀发生变化,先数原有非法前缀就能线性判定。
- 删一个区间只需考虑两个冲突极值的来源,双指针配合两个最值即可。
- 虚拟节点把建中心费用转成边权,也自动满足至少建一个中心的要求。
- 快线按最大道路编号计价,必须保留自环和成环边的作用;重构树恢复层次后,奇度点可在树上贪心配对。