大厂真题 / 拼多多
拼多多 7.19 笔试真题 - 通用
本场考试概述
考试时间:2026年7月19日
考试岗位:通用
难度评级:中等偏难
考点分析:
- 第一题:区间赋值模拟与唯一性判定(难度简单)
- 第二题:排序、线性动态规划与单调队列优化(难度困难)
- 第三题:坐标变换与最长上升子序列(难度中等偏难)
- 第四题:基环树、异或方程与叶子剥离(难度困难)
建议策略:
- 第一题只需一次扫描,先准确拿下;重点是区分“必须覆盖的差异段”和“可以扩展的相同状态段”
- 第三题的严格不等式可以拆成两个维度的严格递增,识别坐标变换后就是标准 LIS
- 第二、四题都需要先证明结构性质再优化实现,时间紧张时应先写清状态或方程,避免在代码细节中反复试错
第 1 题:实例状态发布操作判定
题目描述
有 $n$ 个依次编号的实例,每个实例的状态为 $0$ 或 $1$。发布平台执行了恰好一次操作:选择一个非空连续区间 $[L,R]$ 和目标状态 $v \in {0,1}$,把区间内所有实例都设为 $v$。
区间中可以包含原本就等于 $v$ 的实例,但整次操作必须至少改变一个实例。已知操作前后的状态串分别为 $A$ 和 $B$,并保证至少存在一种合法操作可以把 $A$ 变成 $B$。
如果合法三元组 $(L,R,v)$ 恰好只有一个,输出这个三元组;否则输出 -1。只要 $L$、$R$、$v$ 中任意一项不同,就视为不同操作。
输入第一行是测试用例数 $T$,满足 $1 \leq T \leq 10$。每组数据依次给出 $n$、长度为 $n$ 的字符串 $A$ 和 $B$。所有测试用例的 $n$ 之和不超过 $2 \times 10^5$。
样例
输入
6
5
00000
01110
5
01000
01110
6
111111
100001
7
0001000
0111110
1
0
1
5
00010
01110
输出
2 4 1
-1
2 5 0
2 6 1
1 1 1
-1
思路分析
第一步:找出操作必须覆盖的部分
扫描所有满足 $A_i \ne B_i$ 的位置。它们一定被本次操作真正改变,因此必须全部位于操作区间内。
记最左、最右的差异位置为 left 和 right。题目保证操作存在,而且操作至少改变一个实例,所以差异位置一定非空。目标状态也随之确定:
合法区间必须覆盖完整的 [left, right]。如果两个差异位置之间夹着未变化位置,它也只能已经等于 $v$;否则就不可能用一次区间赋值得到 $B$。
第二步:判断端点能否向外扩展
差异段外的某个位置没有发生变化,因此有 $A_i=B_i$。如果 left 左侧紧邻位置的最终状态也等于 $v$,把它一并纳入区间不会改变结果,于是至少存在两个不同的左端点,操作不唯一。
右侧完全对称:如果 right 右侧紧邻位置的状态等于 $v$,右端点也有多种选择。
只检查紧邻位置就足够。如果紧邻位置不能纳入区间,更远的位置也不可能跨过它被纳入同一个连续区间;如果紧邻位置可以纳入,则已经能构造第二种合法操作。
第三步:得到唯一性条件
因此,操作唯一当且仅当:
- 差异段左侧越界,或紧邻状态不等于 $v$
- 差异段右侧越界,或紧邻状态不等于 $v$
以第二组样例为例,真正发生变化的是第 $3$、$4$ 个位置,目标状态是 $1$。但最终串的第 $2$ 个位置也为 $1$,所以 [3,4] 和 [2,4] 都能得到同一结果,只能输出 -1。
题解代码
import sys
input = sys.stdin.readline
def solve_case(n, before, after):
left = -1
right = -1
for i in range(n):
if before[i] != after[i]:
if left == -1:
left = i
right = i
target = after[left]
can_expand_left = left > 0 and after[left - 1] == target
can_expand_right = right + 1 < n and after[right + 1] == target
if can_expand_left or can_expand_right:
return "-1"
return f"{left + 1} {right + 1} {target}"
def solve():
test_count = int(input())
answers = []
for _ in range(test_count):
n = int(input())
before = input().strip()
after = input().strip()
answers.append(solve_case(n, before, after))
print("\n".join(answers))
solve()
复杂度分析
时间复杂度:每组数据为 $O(n)$,只需扫描一次两个字符串。
空间复杂度:$O(n)$,用于保存输入的两个状态串;除输入外只使用 $O(1)$ 额外空间。
第 2 题:GPU 批处理调度最少批次
题目描述
服务器中有 $N$ 个待处理请求,第 $i$ 个请求的 Token 长度为 $L_i$。请求可以任意重排,并按照以下规则分成若干批次:
- 每个批次最多包含 $C$ 个请求
- 同一批次内最长请求与最短请求的长度差不超过 $K$
- 最多可以直接丢弃 $M$ 个请求,被丢弃的请求不需要处理
求处理其余请求至少需要多少个批次。若允许丢弃全部请求,答案可以为 $0$。
输入第一行是测试用例数 $T$,满足 $1 \leq T \leq 3$。每组数据第一行给出 $N,M,K,C$,第二行给出 $N$ 个请求长度,其中:
\[1 \leq N \leq 10^5,\quad 0 \leq M \leq 50,\quad 0 \leq K \leq 10^9,\quad 1 \leq C \leq N\] \[1 \leq L_i \leq 10^9\]样例
输入
2
5 1 1 2
10 11 15 16 17
4 2 10 3
1 100 2 200
输出
2
1
第一组可以丢弃长度 $15$,再把 $(10,11)$ 和 $(16,17)$ 分成两批。第二组丢弃 $100$、$200$ 后,$(1,2)$ 可以放在同一批。
思路分析
第一步:排序后消除交叉分组
请求顺序可以任意调整,先将长度从小到大排序。
考虑任意最优方案。按照每个批次的最小长度给批次排序后,如果两个批次的元素在有序数组中交叉,可以把两批元素合并排序,再按原来的批次大小切成前后两段。两批的元素数量不变,各自的新极差不会比原来更大,因此合法性和批次数都不变。反复交换后,每个批次占据一段连续的保留元素。
如果某一批内部还夹着被丢弃的元素,可以保留这个中间元素,改为丢弃该批最左或最右的一个已保留元素。批内数量不变,极差只会缩小。重复这个交换后,总能得到一个同样优的方案,使每个批次对应原排序数组中的连续区间,被丢弃元素只出现在批次之间或整个序列两端。
这一步把任意集合分组转化成了有序前缀上的动态规划。
第二步:定义恰好丢弃数量的状态
令 $f_j(i)$ 表示处理完排序后前 $i$ 个请求、其中恰好丢弃 $j$ 个请求时的最少批次数。
第 $i$ 个请求有两种处理方式:
- 丢弃它,从 $f_{j-1}(i-1)$ 转移
- 保留它,并让最后一个批次包含下标区间 $[p,i-1]$,从 $f_j(p)+1$ 转移
第二种转移要求最后一批非空、数量不超过 $C$,并且极差不超过 $K$:
\[i-C \leq p < i,\qquad L_{i-1}-L_p \leq K\]记 first_valid[i - 1] 为满足长度差约束的最小下标,则完整转移为:
对于 $j=0$,只有 $f_0(0)=0$;其余非法状态视为正无穷。最终答案是 $f_0(N),f_1(N),\ldots,f_M(N)$ 中的最小值。
第三步:双指针确定合法批次窗口
固定批次右端点后,随着右端点右移,满足 $L_{i-1}-L_p\leq K$ 的最小 $p$ 只会向右移动。用双指针预处理每个位置的 first_valid,总时间为 $O(N)$。
容量约束给出的下界是 $i-C$。因此 DP 转移中 $p$ 的实际下界是二者较大值,而且这个窗口下界同样单调右移。
第四步:用单调队列维护窗口最小值
固定丢弃数 $j$,按 $i=1,2,\ldots,N$ 计算当前这一层 DP。计算 $f_j(i)$ 前,把候选前缀 $p=i-1$ 加入队列;队列按 $f_j(p)$ 从小到大排列,并删除已经滑出合法窗口的下标。队首就是转移所需的最小值。
每个前缀下标在每一层最多入队、出队一次,所以单层是 $O(N)$。层与层之间只保留 previous 和 current 两个数组,不需要保存完整的 $N \times M$ 状态表。
题解代码
import sys
from collections import deque
input = sys.stdin.readline
def min_batches(n, max_drop, max_gap, capacity, lengths):
lengths.sort()
first_valid = [0] * n
left = 0
for right in range(n):
while lengths[right] - lengths[left] > max_gap:
left += 1
first_valid[right] = left
inf = n + 1
previous = [inf] * (n + 1)
answer = inf
for discarded in range(min(max_drop, n) + 1):
current = [inf] * (n + 1)
if discarded == 0:
current[0] = 0
window = deque()
for size in range(1, n + 1):
prefix = size - 1
while window and current[window[-1]] >= current[prefix]:
window.pop()
window.append(prefix)
lower = max(first_valid[size - 1], size - capacity)
while window[0] < lower:
window.popleft()
keep_last = current[window[0]] + 1
drop_last = previous[size - 1] if discarded > 0 else inf
current[size] = min(keep_last, drop_last)
answer = min(answer, current[n])
previous = current
return answer
def solve():
test_count = int(input())
answers = []
for _ in range(test_count):
n, max_drop, max_gap, capacity = map(int, input().split())
lengths = list(map(int, input().split()))
answers.append(str(min_batches(n, max_drop, max_gap, capacity, lengths)))
print("\n".join(answers))
solve()
复杂度分析
时间复杂度:$O(N\log N+N\cdot\min(M,N))$。排序需要 $O(N\log N)$,每个丢弃数对应一轮线性 DP。
空间复杂度:$O(N)$,用于有序数组、窗口左端点、两层 DP 和单调队列。
第 3 题:横版接金币游戏
题目描述
角色可以沿横轴左右移动,最大速度为每秒 $1$ 个单位距离。地图上会出现 $N$ 枚金币,第 $i$ 枚金币在 $T_i$ 秒时出现在坐标 $X_i$,如果没有立即接住就会消失。
接住金币 $A$ 后再接金币 $B$,两次出现的时间间隔必须严格大于两地间的移动时间:
\[T_B-T_A>\lvert X_B-X_A\rvert\]游戏开始前可以在任意位置等待。如果同一时刻、同一坐标出现多枚金币,最多接住其中一枚。求最多能接住多少枚金币。
输入第一行是金币数量 $N$,接下来 $N$ 行每行给出 $T_i,X_i$,其中:
\[1 \leq N \leq 10^5,\quad 1 \leq T_i \leq 10^9,\quad -10^9 \leq X_i \leq 10^9\]样例
输入
5
1 2
5 5
2 3
7 4
6 8
输出
3
一种最优路径是依次接住 $(1,2)$、$(5,5)$、$(7,4)$ 三枚金币。
思路分析
第一步:直接做二维 DP 会超时
若把每枚金币看成一个节点,满足时间条件时从较早金币连向较晚金币,题目就是求最长路径。枚举所有金币对需要 $O(N^2)$,无法处理 $10^5$ 个点。
需要利用移动条件的代数结构,把二维偏序转化成可以排序和二分的问题。
第二步:拆开绝对值不等式
条件
\[T_B-T_A>\lvert X_B-X_A\rvert\]等价于同时满足:
\[T_B-T_A>X_B-X_A\] \[T_B-T_A>-(X_B-X_A)\]移项后得到:
\[T_B+X_B>T_A+X_A\] \[T_B-X_B>T_A-X_A\]令
\[u=T+X,\qquad v=T-X\]那么从金币 $A$ 转移到金币 $B$,恰好等价于 $u_B>u_A$ 且 $v_B>v_A$。原问题变成了二维严格递增链的最大长度。
第三步:排序一维,在另一维求严格 LIS
先按 $u$ 升序排序,再对排序后的 $v$ 序列求最长严格上升子序列。
需要特别处理 $u$ 相同的金币。它们不能出现在同一条严格递增链中,所以同一 $u$ 内必须按 $v$ 降序排列。这样即使两个点的 $v$ 递增,排序后也不会被严格 LIS 同时选中。
求严格 LIS 时,tails[k] 表示长度为 k + 1 的递增子序列能取得的最小结尾。对每个 $v$ 使用 bisect_left 找第一个不小于它的位置并替换;只有比所有结尾都大时才追加。
第四步:手动模拟样例
五枚金币变换为 $(u,v)$ 并按规则排序后,$v$ 序列是:
-1, -1, 0, 3, -2
其中一条最长严格上升子序列是 -1, 0, 3,长度为 $3$,对应样例答案。
题解代码
import sys
from bisect import bisect_left
input = sys.stdin.readline
def solve():
n = int(input())
transformed = []
for _ in range(n):
time, position = map(int, input().split())
transformed.append((time + position, time - position))
transformed.sort(key=lambda point: (point[0], -point[1]))
tails = []
for _, value in transformed:
index = bisect_left(tails, value)
if index == len(tails):
tails.append(value)
else:
tails[index] = value
print(len(tails))
solve()
复杂度分析
时间复杂度:$O(N\log N)$,排序和每次二分更新 LIS 都需要对数时间。
空间复杂度:$O(N)$,用于保存变换后的坐标和 tails 数组。
第 4 题:单环图告警翻转维护
题目描述
一套告警网络由 $N$ 个节点和 $N$ 条无向边组成。图连通、没有自环和重边,因此恰好包含一个简单环。
节点 $i$ 的初始告警状态为 $b_i \in {0,1}$。第 $i$ 条边连接 $u_i$、$v_i$,维护费用为 $c_i$。选择维护一条边时,它的两个端点都会翻转状态。每条边最多选择一次,边的选择顺序不影响结果。
求使所有节点最终都变为 $0$ 的最小费用,以及达到最小费用的方案数量。如果不存在可行方案,输出 -1 0。
输入第一行是测试用例数 $T$,满足 $1 \leq T \leq 3$。每组数据先给出节点数 $N$ 和 $N$ 个初始状态,随后给出恰好 $N$ 条边,其中:
\[3 \leq N \leq 2 \times 10^5,\quad b_i \in \{0,1\},\quad 1 \leq c_i \leq 10^9\]样例
输入
3
5
0 1 1 1 1
1 2 4
2 3 2
3 1 7
3 4 5
4 5 1
4
1 1 1 1
1 2 1
2 3 1
3 4 1
4 1 1
3
1 0 0
1 2 1
2 3 1
3 1 1
输出
3 1
2 2
-1 0
思路分析
第一步:把选择边写成异或方程
设 $x_e=1$ 表示选择边 $e$,否则 $x_e=0$。节点 $i$ 要从初始状态 $b_i$ 变成 $0$,与它相连的被选边数量奇偶性必须满足:
\[\mathop{\mathrm{xor}}_{e\text{ 与 }i\text{ 相连}}x_e=b_i\]每次操作同时翻转两个节点,所以所有状态的异或和不会改变。全零状态的异或和为 $0$,可行的必要条件是初始值中 $1$ 的数量为偶数。
在连通图上,这个条件也充分。可以先删掉一条环边得到一棵树,从叶子到根依次决定父边;最后根节点能否清零只由全部 $b_i$ 的奇偶性决定。
第二步:理解为什么可行时恰好有两个方案
取任意两个可行方案并对它们的选边状态逐边异或。每个节点在差异边集合中的度数都是偶数,因为两个方案在该节点产生的翻转奇偶性相同。
基环树只有一个环。一个所有节点度数都为偶数的边集,要么为空,要么就是完整的唯一环。因此一个可行方案只能对应另一个方案:把环上每条边的选择状态全部取反,非环边保持不变。
所以,可行时恰好有两个方案。只需构造其中一个,再计算翻转整条环后的另一个方案费用。
第三步:叶子剥离找出唯一环
把所有当前度数为 $1$ 的节点加入队列,反复删除叶子并减少相邻节点的度数。最终未被删除的节点都在唯一环上,两端都在环上的边就是环边。
整个过程中每个节点、每条边只被处理常数次。
第四步:删一条环边,在树上构造特解
任意跳过一条环边,剩下的图是一棵生成树。以节点 $1$ 为根做 BFS,记录每个节点的父节点、父边和遍历顺序。
然后逆序处理节点。对于非根节点 $x$:
- 如果当前状态为 $0$,不选择父边
- 如果当前状态为 $1$,只有选择它的父边才能在不影响已处理子树的前提下把它清零,同时父节点状态翻转
这会唯一确定生成树上的所有边,得到一个可行特解及费用 base_cost。
第五步:比较两个方案的费用
设特解中已选择环边的费用和为 $S_{\text{in}}$,未选择环边的费用和为 $S_{\text{out}}$。把整条环的选择状态取反后,另一个方案的费用为:
\[\text{other\_cost}=\text{base\_cost}-S_{\text{in}}+S_{\text{out}}\]两者费用不同,较小者对应唯一最优方案;费用相同时,两种方案都最优,方案数为 $2$。
题解代码
import sys
from collections import deque
input = sys.stdin.readline
def solve_case(n, state, edges):
if sum(state) % 2 == 1:
return "-1 0"
graph = [[] for _ in range(n)]
degree = [0] * n
for edge_id, (u, v, _) in enumerate(edges):
graph[u].append((v, edge_id))
graph[v].append((u, edge_id))
degree[u] += 1
degree[v] += 1
removed = [False] * n
queue = deque(i for i in range(n) if degree[i] == 1)
while queue:
node = queue.popleft()
removed[node] = True
for neighbor, _ in graph[node]:
if not removed[neighbor]:
degree[neighbor] -= 1
if degree[neighbor] == 1:
queue.append(neighbor)
on_cycle = [not flag for flag in removed]
cycle_edge = [on_cycle[u] and on_cycle[v] for u, v, _ in edges]
skipped_edge = next(i for i, flag in enumerate(cycle_edge) if flag)
parent = [-2] * n
parent_edge = [-1] * n
order = []
parent[0] = -1
queue = deque([0])
while queue:
node = queue.popleft()
order.append(node)
for neighbor, edge_id in graph[node]:
if edge_id == skipped_edge or parent[neighbor] != -2:
continue
parent[neighbor] = node
parent_edge[neighbor] = edge_id
queue.append(neighbor)
remaining = state[:]
selected = [False] * n
base_cost = 0
for node in reversed(order):
if parent[node] == -1:
continue
if remaining[node] == 1:
edge_id = parent_edge[node]
selected[edge_id] = True
base_cost += edges[edge_id][2]
remaining[node] = 0
remaining[parent[node]] ^= 1
selected_cycle_cost = sum(
edges[i][2]
for i in range(n)
if cycle_edge[i] and selected[i]
)
unselected_cycle_cost = sum(
edges[i][2]
for i in range(n)
if cycle_edge[i] and not selected[i]
)
other_cost = base_cost - selected_cycle_cost + unselected_cycle_cost
if base_cost < other_cost:
return f"{base_cost} 1"
if other_cost < base_cost:
return f"{other_cost} 1"
return f"{base_cost} 2"
def solve():
test_count = int(input())
answers = []
for _ in range(test_count):
n = int(input())
state = list(map(int, input().split()))
edges = []
for _ in range(n):
u, v, cost = map(int, input().split())
edges.append((u - 1, v - 1, cost))
answers.append(solve_case(n, state, edges))
print("\n".join(answers))
solve()
复杂度分析
时间复杂度:每组数据为 $O(N)$,叶子剥离、BFS、逆序构造和费用统计都只线性扫描图。
空间复杂度:$O(N)$,用于邻接表及若干节点、边状态数组。
小结
- 第一题把一次区间赋值拆成“必须覆盖的差异段”和“可选的扩展段”,唯一性最终只取决于差异段两侧能否继续纳入目标状态
- 第二题先用交换论证把任意分组整理成有序连续区间,再用单调队列把 DP 的窗口最小值转移降到均摊常数时间
- 第三题通过 $u=T+X$、$v=T-X$ 将严格移动条件变成二维严格递增,排序后归约为一维严格 LIS
- 第四题把边操作视为异或方程,利用基环树只有一个自由环的性质,只需构造一个特解并翻转整环即可得到全部方案