大厂真题 / 拼多多
拼多多 2026-8-23 笔试真题 - 算法岗
本场考试概述
考试时间:2026 年 8 月 23 日
考试岗位:算法岗
难度评级:中等偏难
考点分析:
- 第 1 题:贪心、翻转奇偶性
- 第 2 题:二分答案、差分贪心
- 第 3 题:分层图、Dijkstra 最短路
- 第 4 题:镜像点、带权并查集、整数奇偶性
第 1 题:展廊灯带后缀点亮
题目描述
一条灯带分成 $m$ 段,从左到右编号为 $1$ 到 $m$。第 $i$ 段的初始状态为 $b_i$:亮记作 $1$,灭记作 $0$。
值班员从左向右走。到达第 $i$ 段时,可以按一次该段的总控,将后缀
\[b_i,b_{i+1},\ldots,b_m\]全部取反。走过某段后不能回头,每个下标最多操作一次。求使所有灯段最终都为 $1$ 的最少操作次数。
输入描述
第一行输入整数 $m$。
第二行输入 $m$ 个整数 $b_1,b_2,\ldots,b_m$,每个数为 $0$ 或 $1$。
数据范围:
\[1\le m\le 10^5.\]输出描述
输出一个非负整数,表示最少操作次数。
样例 1
输入
1
1
输出
0
样例 2
输入
3
0 1 0
输出
3
依次在第 $1,2,3$ 段按下总控,状态依次变为 1 0 1、1 1 0、1 1 1。
样例 3
输入
4
0 0 1 1
输出
2
思路分析
走到第 $i$ 段时,后面的操作再也无法影响它。因此,如果它在此前若干次翻转后为 $0$,就必须在这里操作;如果为 $1$,则不能操作。每一步的选择都被最终目标唯一确定,所以该贪心方案必然最优。
只需记录此前操作次数的奇偶性。设已经操作了 $c$ 次,则当前位置的实际状态为
\[b_i'=b_i\mathbin{\oplus}(c\bmod 2).\]若 $b_i’=0$,令 $c\leftarrow c+1$。扫描结束后的 $c$ 即为答案。
正确性证明
处理第 $i$ 段时,所有下标小于 $i$ 的决策已经结束,而下标大于 $i$ 的操作不会影响第 $i$ 段。故使第 $i$ 段最终为 $1$ 的唯一方法是:当前为 $0$ 时操作,当前为 $1$ 时不操作。算法对每一段都执行这个唯一合法决策,因此最终一定得到全 $1$,且任何可行方案都必须进行同样的操作,算法的操作次数最少。
ACM Python 代码
import sys
def solve():
data = list(map(int, sys.stdin.buffer.read().split()))
m = data[0]
lights = data[1:1 + m]
presses = 0
for state in lights:
current = state ^ (presses & 1)
if current == 0:
presses += 1
print(presses)
if __name__ == "__main__":
solve()
复杂度分析
时间复杂度:$O(m)$。
空间复杂度:$O(1)$(不计输入数组)。
易错点
- 只需维护翻转次数的奇偶性,不必真的修改整个后缀。
- 当前状态已经为 $1$ 时不能多按;额外操作会让该位置永久变为 $0$。
- 全为 $1$ 时答案为 $0$。
第 2 题:护栏补强
题目描述
一条护栏分成 $L$ 段,第 $i$ 段的初始高度为 $h_i$。最多可以施工 $T$ 次。每次选择一个长度不超过 $W$ 的非空连续区间 $[p,q]$,满足
\[1\le p\le q\le L,\qquad q-p+1\le W,\]并使区间内每一段的高度增加 $1$。求施工后整条护栏最小高度的最大可能值。
输入描述
第一行输入三个整数 $L,T,W$。
第二行输入 $L$ 个整数 $h_1,h_2,\ldots,h_L$。
数据范围:
\[1\le L,W\le 10^9,\qquad 0\le T\le 10^9,\] \[0\le h_i\le 10^9.\]输入保证第二行给出恰好 $L$ 个高度。
输出描述
输出补强后最小高度的最大可能值。
样例 1
输入
4 2 2
3 1 1 3
输出
3
两次施工都选择区间 $[2,3]$,最终高度为 $(3,3,3,3)$。
样例 2
输入
1 10 1
5
输出
15
样例 3
输入
3 0 2
4 2 8
输出
2
思路分析
二分最终的最小高度 $x$。若能用不超过 $T$ 次施工使所有位置至少为 $x$,那么所有更小的目标也都能达到,判定具有单调性。
判定时从左向右扫描。设仍覆盖当前位置的施工累计增量为 active,则当前位置高度为 $h_i+active$。若它小于 $x$,缺口
必须立即补足。把这些施工的左端点放在 $i$,并尽量向右覆盖 $W$ 段,既不会浪费在已经处理好的左侧,又能最大程度帮助后续位置,因此是最优选择。
用差分数组登记增量在 $i+W$ 处失效,即可让一次判定保持线性。答案下界为 $\min h_i$,上界可取 $\min h_i+T$。
正确性证明
对固定目标 $x$,从左向右考虑位置 $i$。此前位置已经达标,而以后才开始的区间无法覆盖 $i$。因此当 $h_i+active<x$ 时,任何可行方案都至少还要使用 need 次覆盖 $i$ 的施工。算法恰好使用 need 次,没有额外消耗。
在所有能覆盖 $i$ 的新增区间中,把左端点放在 $i$ 能覆盖最远的右侧且不影响已经处理的位置,所以不会比任何其他放置方式更差。归纳可得,算法达到目标 $x$ 所用的施工数最少,判定结果正确。由于可行性关于 $x$ 单调,二分得到的最大可行值就是答案。
ACM Python 代码
import sys
def feasible(heights, operations, width, target):
n = len(heights)
expire = [0] * (n + 1)
active = 0
used = 0
for i, height in enumerate(heights):
active += expire[i]
current = height + active
if current >= target:
continue
need = target - current
used += need
if used > operations:
return False
active += need
end = min(n, i + width)
expire[end] -= need
return True
def solve():
data = list(map(int, sys.stdin.buffer.read().split()))
length, operations, width = data[:3]
heights = data[3:3 + length]
low = min(heights)
high = low + operations
while low < high:
middle = (low + high + 1) // 2
if feasible(heights, operations, width, middle):
low = middle
else:
high = middle - 1
print(low)
if __name__ == "__main__":
solve()
复杂度分析
设输入实际包含 $L$ 段。
时间复杂度:$O(L\log(T+1))$。
空间复杂度:$O(L)$。
易错点
- 二分求最大可行值时,中点要向上取整。
- 差分撤销位置是
min(L, i + W),区间长度最多为 $W$。 - 单次施工允许覆盖少于 $W$ 段,所以接近右端时仍可从当前位置开始施工。
- $T$ 和高度均可达 $10^9$,累计值需使用 64 位整数;Python 整数可直接处理。
第 3 题:驿站补给最短耗时
题目描述
有 $C$ 个驿站和 $E$ 条双向土路。第 $i$ 条路连接 $x_i$ 与 $y_i$,通行需要消耗 $a_i$ 格能量并花费 $w_i$ 时间。
巡检车要从 $1$ 号驿站到达 $C$ 号驿站。电池容量为 $B$,出发时满电。在第 $j$ 个驿站可以逐格充电,每充 $1$ 格花费 $s_j$ 时间,电量不能超过 $B$。求最短到达时间;若无法到达,输出 $-1$。
输入描述
第一行输入三个整数 $C,E,B$。
第二行输入 $C$ 个整数 $s_1,s_2,\ldots,s_C$。
接下来 $E$ 行,每行输入 $x_i,y_i,a_i,w_i$,描述一条双向土路。
数据范围:
\[2\le C\le10^3,\qquad 1\le E\le10^4,\qquad 1\le B\le10^2,\] \[0\le s_j\le10^2,\] \[1\le x_i,y_i\le C,\qquad 1\le a_i\le10^2,\qquad 1\le w_i\le10^3.\]输出描述
输出从 $1$ 号驿站到 $C$ 号驿站的最短时间;无法到达时输出 $-1$。
样例 1
输入
4 3 5
2 1 9 3
1 2 3 4
2 3 3 5
3 4 2 6
输出
18
先走 $1\to2$,在 2 号驿站充 3 格电,再走 $2\to3\to4$,总时间为 $4+3+5+6=18$。
样例 2
输入
2 1 3
1 1
1 2 4 10
输出
-1
样例 3
输入
3 2 4
0 5 1
1 2 2 3
2 3 2 4
输出
7
思路分析
只记录当前驿站不足以描述状态,因为后续可走的道路还取决于剩余电量。建立分层图状态 $(v,b)$,表示位于驿站 $v$、剩余 $b$ 格电,其中 $0\le b\le B$。
有两类转移:
-
若 $b<B$,在原地充一格电:
\[(v,b)\to(v,b+1),\qquad \text{代价 }s_v.\] -
对道路 $(v,u,a,w)$,若 $b\ge a$,则可以通行:
\[(v,b)\to(u,b-a),\qquad \text{代价 }w.\]
所有边权非负,因此从起点 $(1,B)$ 运行 Dijkstra。第一次从堆中取出任意状态 $(C,b)$ 时,其距离就是答案。能耗超过容量 $B$ 的道路永远不可通行,可以在读入时丢弃。
正确性证明
任意真实行程都由“充一格电”和“通过一条道路”组成,且每一步都对应分层图中的一条边,累计时间等于路径权值。反之,分层图中的每条边都满足容量或能耗条件,因此任意图上路径都对应一段合法行程。于是原问题与从 $(1,B)$ 到任意 $(C,b)$ 的最短路完全等价。所有转移代价非负,Dijkstra 正确求出该最短路。
ACM Python 代码
import heapq
import sys
def solve():
input = sys.stdin.buffer.readline
station_count, edge_count, capacity = map(int, input().split())
charge_time = [0] + list(map(int, input().split()))
graph = [[] for _ in range(station_count + 1)]
for _ in range(edge_count):
x, y, energy, travel_time = map(int, input().split())
if energy <= capacity:
graph[x].append((y, energy, travel_time))
graph[y].append((x, energy, travel_time))
width = capacity + 1
infinity = 10**30
distance = [infinity] * ((station_count + 1) * width)
start = width + capacity
distance[start] = 0
heap = [(0, start)]
while heap:
current_time, state = heapq.heappop(heap)
if current_time != distance[state]:
continue
station, battery = divmod(state, width)
if station == station_count:
print(current_time)
return
if battery < capacity:
next_state = state + 1
next_time = current_time + charge_time[station]
if next_time < distance[next_state]:
distance[next_state] = next_time
heapq.heappush(heap, (next_time, next_state))
for neighbor, energy, travel_time in graph[station]:
if battery < energy:
continue
next_state = neighbor * width + battery - energy
next_time = current_time + travel_time
if next_time < distance[next_state]:
distance[next_state] = next_time
heapq.heappush(heap, (next_time, next_state))
print(-1)
if __name__ == "__main__":
solve()
复杂度分析
状态数为 $O(CB)$,分层图中的充电转移为 $O(CB)$,道路转移为 $O(EB)$。时间复杂度:
\[O\bigl((CB+EB)\log(CB)\bigr),\]空间复杂度:$O(CB+E)$。
易错点
- 初始状态是满电的 $(1,B)$,不是 $(1,0)$。
- 到达终点时无需把电量补满;第一次弹出任意终点层即可返回。
- 充电单价可以为 $0$,但边权仍然非负,Dijkstra 依旧适用。
- 一条路的能耗超过 $B$ 时,即使反复充电也无法通过。
第 4 题:和差方程最少标定
题目描述
有 $p$ 个整数量值 $x_1,x_2,\ldots,x_p$,每个值可正、可负、也可为零。给出 $e$ 条记录,每条为以下两种之一:
D u v w:$x_u-x_v=w$;S u v w:$x_u+x_v=w$。
一条记录中的两个下标可以相同。先判断全部记录是否存在一组整数解;若有,再求至少需要实测多少个样品,才能唯一确定全部 $p$ 个值。
输入描述
第一行输入两个整数 $p,e$。
接下来 $e$ 行,每行输入一个大写字母 D 或 S,以及三个整数 $u,v,w$。
数据范围:
\[1\le p\le100000,\qquad 0\le e\le100000,\] \[1\le u,v\le p,\qquad \lvert w\rvert\le10^9.\]输出描述
第一行输出 YES 或 NO,表示是否存在整数解。
若有解,第二行输出最少实测个数;若无解,第二行输出 $-1$。
样例 1
输入
2 2
S 1 2 8
D 1 2 2
输出
YES
0
由 $x_1+x_2=8$、$x_1-x_2=2$ 得 $x_1=5,x_2=3$,无需实测。
样例 2
输入
1 1
S 1 1 5
输出
NO
-1
约束等价于 $2x_1=5$,没有整数解。
样例 3
输入
3 2
D 1 2 1
D 2 3 1
输出
YES
1
思路分析
1. 用镜像点统一和与差
为每个变量 $x_i$ 建立两个点:本体点 $i$ 表示 $x_i$,镜像点 $i’$ 表示 $-x_i$。这样和关系也能改写为差关系:
\[x_u+x_v=w\iff x_u-(-x_v)=w.\]每条记录必须成对加入约束,以保持本体与镜像的对称性:
-
\[x_u-x_v=w,\qquad (-x_u)-(-x_v)=-w;\]D u v w加入 -
\[x_u-(-x_v)=w,\qquad (-x_u)-x_v=-w.\]S u v w加入
2. 带权并查集维护势差
维护
\[pot[z]=value(z)-value(parent(z)).\]路径压缩后,pot[z] 就是点 $z$ 相对其根的值。加入约束 $value(a)-value(b)=c$ 时:若两点已同根,检查 pot[a] - pot[b] == c;否则按大小合并,并计算新挂接根相对父根的势差。
3. 整数解的奇偶判定
若本体 $i$ 与镜像 $i’$ 不连通,对应连通块仍有一个整数平移自由度,实测其中任意一个样品即可确定这一对镜像块。
若 $i$ 与 $i’$ 连通,则路径约束固定了
\[value(i)-value(i')=x_i-(-x_i)=2x_i.\]路径压缩后应计算
\[delta=pot[i]-pot[i'].\]只有当 $delta$ 为偶数时,$x_i=delta/2$ 才是整数;若为奇数,则方程组在有理数范围可能有解,但没有整数解。这里应使用势之差而不是势之和。虽然整数 $a+b$ 与 $a-b$ 奇偶性相同,只检查 % 2 时二者碰巧给出相同结果,但势之和并不等于 $2x_i$,会掩盖公式和后续扩展中的错误。
若该块通过奇偶检查,则整块已被唯一确定,不需要实测。其他未固定的连通块按镜像两两配对,每对只需实测一个样品,因此答案为未固定根数除以 $2$。
正确性证明
镜像点定义保证每条原始记录等价于代码加入的两条差约束,因此原方程组与并查集约束等价。带权并查集始终维护点到父节点的势差;合并不同集合时设置的根间势差使新约束成立,同集合校验则准确识别矛盾。
若某点与其镜像同根,两者之差由路径唯一确定,而该差必须等于 $2x_i$。它为奇数时不存在整数 $x_i$;为偶数时 $x_i$ 唯一确定,并可沿势差确定整个连通块。若二者不同根,这对镜像块保留且仅保留一个自由整数参数,实测其中一个原变量即可确定整对。故算法既能正确判断整数可解性,也能得到最少实测数。
ACM Python 代码
import sys
def solve():
input = sys.stdin.buffer.readline
variable_count, record_count = map(int, input().split())
node_count = 2 * variable_count
parent = list(range(node_count))
size = [1] * node_count
potential = [0] * node_count
def find(x):
path = []
current = x
while parent[current] != current:
path.append(current)
current = parent[current]
root = current
total = 0
for node in reversed(path):
total += potential[node]
potential[node] = total
parent[node] = root
return root
def unite(a, b, difference):
# value(a) - value(b) = difference
root_a = find(a)
root_b = find(b)
if root_a == root_b:
return potential[a] - potential[b] == difference
if size[root_a] < size[root_b]:
parent[root_a] = root_b
potential[root_a] = potential[b] + difference - potential[a]
size[root_b] += size[root_a]
else:
parent[root_b] = root_a
potential[root_b] = potential[a] - difference - potential[b]
size[root_a] += size[root_b]
return True
consistent = True
for _ in range(record_count):
operation, raw_u, raw_v, raw_w = input().split()
u = int(raw_u) - 1
v = int(raw_v) - 1
w = int(raw_w)
if not consistent:
continue
if operation == b"D":
first_ok = unite(u, v, w)
second_ok = unite(u + variable_count, v + variable_count, -w)
else:
first_ok = unite(u, v + variable_count, w)
second_ok = unite(u + variable_count, v, -w)
consistent = first_ok and second_ok
if not consistent:
print("NO")
print(-1)
return
pinned = [False] * node_count
for i in range(variable_count):
root = find(i)
mirror_root = find(i + variable_count)
if root != mirror_root:
continue
# value(i) - value(i') = x_i - (-x_i) = 2*x_i。
delta = potential[i] - potential[i + variable_count]
if delta % 2 != 0:
print("NO")
print(-1)
return
pinned[root] = True
free_roots = 0
for node in range(node_count):
root = find(node)
if root == node and not pinned[root]:
free_roots += 1
print("YES")
print(free_roots // 2)
if __name__ == "__main__":
solve()
复杂度分析
时间复杂度:
\[O\bigl((p+e)\alpha(p)\bigr),\]空间复杂度:$O(p)$。find 使用迭代路径压缩,避免 $p=10^5$ 时递归栈溢出。
易错点
- 每条记录必须同时加入镜像约束,否则最后的连通块不再成镜像对。
- 合并根时,挂接方向不同,根势差公式的符号也不同。
- $i$ 与 $i’$ 同根只说明变量被固定在某个有理数;还必须检查
potential[i] - potential[i']是否为偶数,才能保证它是整数。 - 自环要正常处理:
D i i w仅在 $w=0$ 时可行;S i i w要求 $w$ 为偶数。 - 找根必须在读取
potential前完成路径压缩,使势能表示相对根的值。
小结
- 第 1 题利用“操作只影响右侧”将每一步决策唯一化,只维护翻转奇偶性。
- 第 2 题二分最小高度,并用差分数组在线性时间内完成贪心判定。
- 第 3 题把驿站与剩余电量组成状态,在分层图上运行 Dijkstra。
- 第 4 题引入表示相反数的镜像点,将和、差方程统一成势差约束;整数解还必须单独检查 $2x_i$ 的奇偶性。