大厂真题 / 拼多多

拼多多 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 11 1 01 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$,缺口

\[need=x-h_i-active\]

必须立即补足。把这些施工的左端点放在 $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$。

有两类转移:

  1. 若 $b<B$,在原地充一格电:

    \[(v,b)\to(v,b+1),\qquad \text{代价 }s_v.\]
  2. 对道路 $(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$ 行,每行输入一个大写字母 DS,以及三个整数 $u,v,w$。

数据范围

\[1\le p\le100000,\qquad 0\le e\le100000,\] \[1\le u,v\le p,\qquad \lvert w\rvert\le10^9.\]

输出描述

第一行输出 YESNO,表示是否存在整数解。

若有解,第二行输出最少实测个数;若无解,第二行输出 $-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.\]

每条记录必须成对加入约束,以保持本体与镜像的对称性:

  • D u v w 加入

    \[x_u-x_v=w,\qquad (-x_u)-(-x_v)=-w;\]
  • S u v w 加入

    \[x_u-(-x_v)=w,\qquad (-x_u)-x_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$ 的奇偶性。