大厂真题 / 拼多多
拼多多 8.2 笔试真题 - 通用(研发/算法)
本场考试概述
考试时间:2026年8月2日
考试岗位:通用(研发/算法)
难度评级:中等偏难
考点分析:
- 第一题:前缀和与首次出现位置(难度简单)
- 第二题:贪心构造与可行性判定(难度中等)
- 第三题:分层图与 Dijkstra 最短路(难度中等)
- 第四题:环上独立集覆盖与下界证明(难度困难)
建议策略:
- 第一题是典型的“数量相等”模型,把两类元素分别赋值为 $1$ 和 $-1$ 后线性扫描即可,应优先拿下
- 第二题不要只凭局部最小值直接贪心;每次试放后必须判断剩余多重集合能否接在当前末尾之后
- 第三题的关键是把“优惠券是否使用”纳入状态;第四题则要分别寻找单点、相邻点和整张环带来的必要下界
第 1 题:平衡队伍
题目描述
$n$ 名队员排成一列,每名队员属于 A、B 两种类型之一。教练要选择一段连续区间;如果其中 A 类与 B 类队员数量相同,就称其为平衡队伍。
求平衡队伍的最大长度;如果不存在非空的平衡区间,输出 $0$。
输入第一行是整数 $n$,第二行是长度为 $n$ 且仅含 A、B 的字符串 $s$。
数据范围:$1 \leq n \leq 2 \times 10^5$。
样例 1
输入
4
ABAB
输出
4
整个序列中两种队员各有两名,因此答案为 $4$。
样例 2
输入
3
AAA
输出
0
样例 3
输入
5
AAABB
输出
4
思路分析
第一步:把计数相等转成区间和为零
给 A 赋值 $1$,给 B 赋值 $-1$。一段区间的元素和就是该区间内 A 的数量减去 B 的数量,因此平衡条件等价于区间和为 $0$。
如果直接枚举左右端点,需要 $O(n^2)$ 时间。为了快速得到区间和,引入前缀和:令 $pre_i$ 表示前 $i$ 个位置的权值和,并令 $pre_0=0$。区间 $[l,r]$ 平衡当且仅当
\[pre_r-pre_{l-1}=0,\]也就是 $pre_r=pre_{l-1}$。
第二步:寻找距离最远的相同前缀和
问题已经转化成:在前缀和序列中,寻找值相等且下标距离最大的一对位置。
从左向右扫描。对于每个前缀和值,只记录它第一次出现的位置。以后再次遇到相同值时,用当前位置减去最早位置更新答案。保留更晚的位置不会产生更长区间,所以没有必要。
前缀和一定落在 $[-n,n]$,既可以使用哈希表,也可以使用长度 $2n+1$ 的数组并加上偏移量 $n$。下面使用数组,常数更小。
第三步:处理空前缀与无解情况
扫描前要记录 first[n] = 0,表示和为 $0$ 的空前缀出现在位置 $0$。否则会漏掉从第一个位置开始的平衡区间。
答案初值设为 $0$。如果没有任何相同前缀和形成正长度区间,最终自然输出 $0$,无需额外特判。
以 AAABB 为例,前缀和依次为 $0,1,2,3,2,1$。值 $1$ 最早出现在下标 $1$,又在下标 $5$ 出现,于是得到长度 $5-1=4$。
题解代码
import sys
input = sys.stdin.readline
def solve():
n = int(input())
s = input().strip()
first = [-1] * (2 * n + 1)
offset = n
first[offset] = 0
prefix = 0
answer = 0
for i, ch in enumerate(s, 1):
prefix += 1 if ch == "A" else -1
index = prefix + offset
if first[index] == -1:
first[index] = i
else:
answer = max(answer, i - first[index])
print(answer)
solve()
复杂度分析
时间复杂度:$O(n)$,每名队员只处理一次。
空间复杂度:$O(n)$,用于保存每种前缀和的首次出现位置。
边界情况
- $n=1$ 时不可能选出人数相等的非空区间,答案为 $0$
- 整个字符串平衡时,需要依靠预先记录的空前缀得到答案 $n$
- 全部字符相同时,答案保持为 $0$
第 2 题:评价展示序列
题目描述
商品共有 $n$ 条评价,每条评价的星级是 $1$ 到 $5$ 之间的整数。现在要重新排列这些评价,使任意两条相邻评价的星级不同。
如果合法排列存在,输出字典序最小的排列;否则输出 -1。
两个等长序列按字典序比较:从左向右找到第一个不同的位置,该位置数值较小的序列字典序更小。
输入第一行是整数 $n$,第二行是 $n$ 个星级 $a_i$。
数据范围:$1 \leq n \leq 10^5$,$1 \leq a_i \leq 5$。
样例 1
输入
5
1 1 1 2 3
输出
1 2 1 3 1
样例 2
输入
4
1 1 1 1
输出
-1
样例 3
输入
6
5 5 3 5 3 1
输出
1 5 3 5 3 5
思路分析
第一步:为什么不能只选当前最小值
为了让字典序最小,直觉上应在每一位选择尚未用完、且不等于上一位的最小星级。但直接这样做可能过早消耗少数类元素,导致数量最多的星级在后面无法被隔开。
因此,每次选择候选值后,要判断剩余元素能否组成一个合法后缀。只要后缀仍可行,就可以立即确定这个候选;因为字典序首先由当前位决定,更大的候选不可能更优。
第二步:推导剩余局面的可行条件
设还剩 $m$ 个元素,上一位星级为 last。设剩余出现次数最多的星级为 $x$,其数量为 $c$。
要隔开 $c$ 个相同的 $x$,至少需要 $c-1$ 个其他元素,所以首先必须满足
\[2c-1 \leq m.\]更精确地说,对任意星级 $v\ne\text{last}$,它至多占后缀的奇数位,因此必须满足 $2c_v\le m+1$;对 $v=\text{last}$,后缀首位不能再放它,因此必须满足 $2c_v\le m$。这组条件也是充分的:始终优先放当前剩余数量最多且不同于上一位的星级,就能把各星级依次放入允许的位置。
如果恰好有 $2c-1=m$,后缀只能严格交替成
\[x,\ *,\ x,\ *,\ \ldots,\ *,\ x.\]此时后缀必须以 $x$ 开头。如果 $x=\text{last}$,它无法接在已有前缀之后,局面不可行。因此完整判据为:
- 若 $2c-1>m$,不可行
- 若 $2c-1=m$ 且 $x=\text{last}$,不可行
- 其他情况可行
星级只有五种,所以每次检查计数数组只需常数时间。
第三步:逐位试放并保证字典序最小
当前位依次尝试星级 $1,2,\ldots,5$:
- 跳过计数为零或等于上一位的值
- 暂时把该值计数减一
- 用上述判据检查剩余后缀
- 若可行,就固定当前位;否则恢复计数并尝试更大的值
每一位选择的都是“仍能完成整个序列”的最小值。对任意另一合法答案,若它第一次与本算法不同,本算法在该位置选择的值一定更小,因此所得序列就是全局字典序最小解。
开局可以把 last 设为 $0$,因为它不可能与任何真实星级相等。也应先对完整多重集合做一次可行性检查;若失败,直接输出 -1。
题解代码
import sys
input = sys.stdin.readline
def feasible(count, remaining, last):
if remaining == 0:
return True
max_count = 0
max_value = 0
for value in range(1, 6):
if count[value] > max_count:
max_count = count[value]
max_value = value
if 2 * max_count - 1 > remaining:
return False
if 2 * max_count - 1 == remaining and max_value == last:
return False
return True
def solve():
n = int(input())
stars = list(map(int, input().split()))
count = [0] * 6
for value in stars:
count[value] += 1
if not feasible(count, n, 0):
print(-1)
return
answer = []
last = 0
for position in range(n):
for value in range(1, 6):
if count[value] == 0 or value == last:
continue
count[value] -= 1
remaining = n - position - 1
if feasible(count, remaining, value):
answer.append(value)
last = value
break
count[value] += 1
print(*answer)
solve()
复杂度分析
时间复杂度:$O(n)$。每个位置至多尝试 $5$ 个值,每次判定也只扫描 $5$ 种星级。
空间复杂度:$O(n)$,答案数组占 $O(n)$;计数数组只占常数空间。
边界情况
- 只有一条评价时,它本身就是答案
- 某星级数量超过 $\lceil n/2 \rceil$ 时一定无解
- 数量最多的星级恰好占满所有奇数位时,要特别检查它是否与已构造前缀的末尾相同
- 输入次序不影响答案,只需要保留五种星级的出现次数
第 3 题:多多送快递
题目描述
$n$ 个城市之间有 $m$ 条有向运输线路。每条线路由城市 $u$ 指向城市 $v$,邮费为正整数 $w$。
商品要从城市 $1$ 送到城市 $n$。现在有一张免邮券,可以把所经过的任意一条线路的邮费变为 $0$;也可以不使用。求从城市 $1$ 到城市 $n$ 的最小总邮费。如果无法到达,输出 -1。
输入第一行是 $n,m$,接下来 $m$ 行每行给出 $u,v,w$。
数据范围:$2 \leq n \leq 10^5$,$0 \leq m \leq 2 \times 10^5$,$1 \leq u,v \leq n$,$1 \leq w \leq 10^4$。
样例
输入
4 4
1 2 2
1 3 5
2 4 3
3 4 1
输出
1
把免邮券用在边 $1\to3$ 上,再支付边 $3\to4$ 的邮费 $1$,总费用最小。
思路分析
第一步:同一城市需要区分两种状态
普通最短路只需记录“到达城市 $u$ 的最小花费”。但本题中,以相同花费到达同一城市时,手里是否还保留免邮券会影响后续决策,二者不能合并。
因此把每个城市拆成两层:
- 状态 $(u,0)$:到达 $u$ 时尚未使用免邮券
- 状态 $(u,1)$:到达 $u$ 时已经使用免邮券
这样共有 $2n$ 个状态。
第二步:把原图边转换为状态转移
对于原图中的有向边 $u\to v$,边权为 $w$:
- 从 $(u,0)$ 正常付费到 $(v,0)$,代价为 $w$
- 从 $(u,1)$ 正常付费到 $(v,1)$,代价为 $w$
- 从 $(u,0)$ 使用免邮券到 $(v,1)$,代价为 $0$
不存在从已用券层返回未用券层的边,也不存在第二次免费跨层,所以这个模型天然保证券至多使用一次。
第三步:在分层图上运行 Dijkstra
所有转移边权都非负,可以从 $(1,0)$ 出发运行 Dijkstra。分层图无需显式建立:遍历城市 $u$ 的原始出边时,根据当前状态实时执行上述一到两种松弛即可。
最终答案是
\[\min(dist_{n,0},dist_{n,1}),\]因为未使用券到达终点也属于合法方案。若两者都仍为无穷大,则输出 -1。
直接枚举哪条边免费,再为每条边单独运行最短路,会产生约 $O(m^2\log n)$ 的代价;分层图只把点数和边数扩大常数倍。
题解代码
import sys
from heapq import heappop, heappush
input = sys.stdin.readline
INF = 10**30
def solve():
n, m = map(int, input().split())
graph = [[] for _ in range(n + 1)]
for _ in range(m):
u, v, weight = map(int, input().split())
graph[u].append((v, weight))
dist = [[INF, INF] for _ in range(n + 1)]
dist[1][0] = 0
heap = [(0, 1, 0)]
while heap:
current_dist, u, used = heappop(heap)
if current_dist != dist[u][used]:
continue
for v, weight in graph[u]:
paid_dist = current_dist + weight
if paid_dist < dist[v][used]:
dist[v][used] = paid_dist
heappush(heap, (paid_dist, v, used))
if used == 0 and current_dist < dist[v][1]:
dist[v][1] = current_dist
heappush(heap, (current_dist, v, 1))
answer = min(dist[n])
print(-1 if answer == INF else answer)
solve()
复杂度分析
时间复杂度:$O((n+m)\log n)$。分层后的状态数和转移数都只是原图的常数倍。
空间复杂度:$O(n+m)$,用于邻接表、距离数组与优先队列。
边界情况
- 图是有向图,不能把线路反向加入邻接表
- 自环与重边可以直接交给 Dijkstra 处理
- 路径费用可能超过 $32$ 位整数范围;Python 整数可自动扩展
- 终点不可达时,两层距离都会保持为无穷大
第 4 题:环形分厂协调补货
题目描述
$n$ 个分仓沿环形物流干线排列,编号为 $1$ 到 $n$。除相邻编号外,分仓 $1$ 与分仓 $n$ 也相邻。分仓 $i$ 需要补货 $a_i$ 次。
每个补货班次可以选择若干分仓,并让每个被选分仓完成一次补货。同一班次内,任何两个相邻分仓都不能同时被选择。每个分仓必须恰好被选择 $a_i$ 次。
求完成所有需求所需的最少班次数。
输入第一行是测试用例数 $T$。每组数据先给出 $n$,再给出 $n$ 个非负整数 $a_i$。保证所有测试用例的 $n$ 之和不超过 $2\times10^6$。
数据范围:$1 \leq n \leq 2\times10^5$,$0 \leq a_i \leq 10^9$,且 $\sum n \leq 2\times10^6$。
样例 1
输入
1
3
1 1 1
输出
3
三个分仓两两相邻,每班最多选择一个分仓。
样例 2
输入
1
4
3 0 3 0
输出
3
分仓 $1$ 和 $3$ 不相邻,可以在三个班次中始终同时选择二者。
样例 3
输入
1
6
1 2 3 1 2 3
输出
5
例如可以依次选择分仓集合 ${2,5}$、${1,3,5}$、${3,6}$、${3,6}$、${2,4,6}$,每个集合都是环上的独立集,累计次数恰好满足需求。
思路分析
每个班次选择的是环上的一个独立集。题目等价于:用尽量少的独立集对各顶点进行带重数覆盖,使顶点 $i$ 总共出现 $a_i$ 次。
关键不是模拟每一个班次,而是找到班次数 $k$ 必须满足的全部瓶颈。
第一步:单个分仓给出的下界
同一分仓在一个班次中最多被补货一次,所以至少需要
\[L_1=\max_i a_i\]个班次。
第二步:相邻分仓给出的下界
任意相邻的两个分仓不能出现在同一班次。它们的补货次数必须占用互不重合的班次,因此
\[L_2=\max_i(a_i+a_{i+1}),\]其中下标按环处理,即还要检查 $a_n+a_1$。
事实上,当 $n$ 为偶数时,环是二分图,这类相邻约束已经足够;$L_2$ 也自然不小于 $L_1$。
第三步:整张环的容量下界
一个长度为 $n$ 的环,其独立集最多包含 $\lfloor n/2\rfloor$ 个顶点。设总需求为
\[S=\sum_{i=1}^{n}a_i,\]则每个班次最多完成 $\lfloor n/2\rfloor$ 次补货,因而还需要
\[L_3=\left\lceil\frac{S}{\lfloor n/2\rfloor}\right\rceil\]个班次。
这条约束主要在奇数环上发挥作用。例如三角形每班只能选择一个点,单看相邻两点之和无法覆盖三个点的总需求。
第四步:为什么取三条下界的最大值就够
这里可以直接使用环图的整数加权着色定理:偶环的加权色数等于最大相邻需求和;奇环 $C_{2h+1}$ 的加权色数等于最大相邻需求和与 $\lceil S/h\rceil$ 中的较大者。每个班次对应一种颜色,同色顶点构成独立集,而顶点 $i$ 需要获得 $a_i$ 种颜色,因此本题正是该定理的加权着色模型。
该定理不仅给出必要下界,也保证整数需求可以分解为相应数量的独立集。若某个分解让顶点出现次数超过需求,还可以从部分独立集中删去该顶点;删除不会破坏独立性,所以能进一步调整为恰好出现 $a_i$ 次。
因此,只要班次数 $k$ 同时满足
\[k\geq L_1,\qquad k\geq L_2,\qquad k\geq L_3,\]就能把每个分仓的 $a_i$ 次需求安排到 $k$ 个班次中,并保证每个班次选择的都是独立集。因此答案就是三者最大值。这里 $L_1$ 对 $n\ge2$ 已包含在 $L_2$ 中,代码保留它只是为了让三个下界的含义更直观。
对于偶数环,$L_3$ 不会比相邻约束更强。因为可以把环边交替分成两组完美匹配,而任意一组匹配上的相邻需求和之和都是 $S$;若每条边的需求和均不超过 $L_2$,便有 $S\leq(n/2)L_2$。奇数环少了二分图结构,整体容量约束正好补足这一缺口。
第五步:单点环必须特判
$n=1$ 时没有其他分仓与它冲突,每班都能选择唯一分仓,答案直接是 $a_1$。此时 $\lfloor n/2\rfloor=0$,若套用容量公式会发生除零。
计算其余情况时只需一次线性扫描,求总和、单点最大值和相邻两点之和最大值,再做一次向上取整:
\[\left\lceil\frac{S}{h}\right\rceil=\left\lfloor\frac{S+h-1}{h}\right\rfloor,\]其中 $h=\lfloor n/2\rfloor$。
题解代码
import sys
input = sys.stdin.readline
def minimum_shifts(demand):
n = len(demand)
if n == 1:
return demand[0]
total = sum(demand)
max_single = max(demand)
max_adjacent = 0
for i in range(n):
adjacent_sum = demand[i] + demand[(i + 1) % n]
max_adjacent = max(max_adjacent, adjacent_sum)
capacity = n // 2
capacity_bound = (total + capacity - 1) // capacity
return max(max_single, max_adjacent, capacity_bound)
def solve():
test_cases = int(input())
answers = []
for _ in range(test_cases):
n = int(input())
demand = list(map(int, input().split()))
answers.append(minimum_shifts(demand))
print("\n".join(map(str, answers)))
solve()
复杂度分析
时间复杂度:每组为 $O(n)$;所有测试用例合计为 $O(\sum n)$。
空间复杂度:每组为 $O(n)$,用于保存当前需求数组。
边界情况
- $n=1$ 时答案是唯一分仓的需求量,必须避免容量公式除零
- 所有 $a_i=0$ 时答案为 $0$
- 必须计算首尾相邻的一对 $a_n+a_1$
- 总需求可能达到 $10^{14}$ 量级,实现时需要使用足够宽的整数类型
- $n=2$ 时两个分仓互相冲突,答案退化为 $a_1+a_2$
小结
- 第一题把两类元素分别映射为 $1$ 和 $-1$,将“数量相等”转化为“相同前缀和”,只保留每个前缀和的最早位置
- 第二题逐位选择最小星级,并用“最大频次是否还能被其他元素隔开”的充要条件保护后续可行性
- 第三题把每个城市拆成“未用券”和“已用券”两层,在分层图上一次 Dijkstra 即可处理任意一条边免费
- 第四题从单点、相邻点和整张环三个角度推导班次数下界,利用环图结构说明三条约束的最大值就是最优答案