大厂真题 / 拼多多

拼多多通用岗 2026-10-25 笔试真题

本场考试概述

考试时间:2026 年 10 月 25 日

考试岗位:通用岗(未区分具体岗位)

难度评级:中等偏难

考点分析:

  • 第一题:免费操作的不变量、排序后的目标位置。
  • 第二题:大根堆、强制切分与贪心匹配。
  • 第三题:区间成本单调性、双指针与单调队列。
  • 第四题:离线离散化、区间加与全局最大值。

建议策略:前两题先证明贪心或计数结论,不必模拟完整调度过程。后两题先写出维护对象的数学表达式,再选择数据结构;尤其要区分检查费用与模式切换费用、商品位置与价格窗口。


第 1 题:列车排序的最少耗电量

题目描述

一列火车有 $N$ 节车厢,第 $i$ 节的卸载优先级为 $A_i$,所有优先级互不相同。需要将车厢按优先级从左到右严格递增排列。

允许任意次使用以下两种操作:

  1. 反转连续 2 节车厢,消耗 1 单位电。
  2. 反转连续 3 节车厢,不消耗电。

每次可以选择任意合法的连续位置,求完成排序所需的最少电量。

输入描述

第一行输入测试组数 $T$。接下来每组输入一个整数 $N$,随后输入 $N$ 个优先级;题目给出的格式中,每组这些数字位于同一行。

约束为 $1\le T\le10$,$1\le N\le10^5$,$0\le A_i\le10^9$,每组的优先级两两不同。

输出描述

每组输出一个整数,表示最少耗电量。

样例

输入

2
4 2 1 3 4
5 10 8 5 3 2

输出

1
0

第一组交换前两节即可。第二组虽然逆序,但每节车厢与其排序后的目标位置具有相同奇偶性,免费操作足够完成调整。

思路分析

第一步:只保留大小关系。

把数组按优先级排序。某元素在排序结果中的下标就是它的目标位置,称为 rank。使用统一的 0 基下标,只需讨论原下标 index 与目标下标的关系。

第二步:识别免费操作能做到什么。

连续三节 $(x,y,z)$ 反转为 $(z,y,x)$,实际上只交换下标 $i$ 与 $i+2$。它不会改变任何元素所在位置的奇偶性。

反过来,所有偶数下标形成一条间隔为 2 的链,奇数下标也形成一条链。三节反转就是各链上的相邻交换,所以同一奇偶类内可以免费实现任意排列。问题只剩下:有多少元素必须从一个奇偶类移动到另一个奇偶类。

第三步:求下界并构造达到下界的方案。

设当前位置与目标位置奇偶性不同的元素有 $m$ 个。付费操作交换相邻两节,至多纠正两个元素,因此至少付费 $m/2$ 次。

偶数位置的数量固定,因此“应去奇数位置却在偶数位置”和“应去偶数位置却在奇数位置”的元素数量必定相同。每次从两类错位元素中各选一个,通过免费操作把它们移到一对相邻位置,再付费交换,恰好修复两个错位。重复后再免费整理各奇偶类内部的顺序。

正确性证明

免费操作保持奇偶类,故每个奇偶错位元素都必须参与付费交换,单次最多修复两个,得到 $m/2$ 的下界。上述配对构造每次正好修复两个,且免费重排不会制造新的奇偶错位,最终恰用 $m/2$ 次付费操作完成排序。上下界相等,因此答案为 $m/2$。

题解代码

import sys

input = sys.stdin.readline


def min_power(priorities):
    order = sorted(range(len(priorities)), key=priorities.__getitem__)
    mismatch = sum((rank - index) % 2 for rank, index in enumerate(order))
    return mismatch // 2


def solve():
    numbers = (int(token) for line in sys.stdin for token in line.split())
    t = next(numbers)
    for _ in range(t):
        n = next(numbers)
        priorities = [next(numbers) for _ in range(n)]
        print(min_power(priorities))


if __name__ == '__main__':
    solve()

复杂度分析

时间复杂度:单组 $O(N\log N)$,排序为主要开销;多组累加各组的排序代价。

空间复杂度:单组 $O(N)$,用于优先级数组、排序下标和排序辅助空间。

易错点

不能用优先级本身的奇偶性判断,应比较元素的原下标与排序后的下标。本题求的是耗电量,不是所有操作的最少次数,不能直接套逆序对计数。


第 2 题:绳子对半切割能否得到指定长度

题目描述

初始只有一根长度为 $L$ 的绳子。每次选择一段长度 $x\ge2$ 的现有绳子,将其切成长度为 $\lfloor x/2\rfloor$ 和 $\lceil x/2\rceil$ 的两段;长度为 1 的绳子不能再切。

可以切割任意次,也可以不切。判断最终能否恰好得到 $n$ 根绳子,其长度多重集为 $a_1,\ldots,a_n$。不允许拼接,也不允许留下多余绳子;目标长度的排列顺序没有要求。

输入描述

第一行输入测试组数 $T$。每组第一行输入 $L,n$,第二行输入 $n$ 个目标长度。

约束为 $1\le T\le50$,$1\le L\le10^{14}$,$1\le n\le10^5$,$1\le a_i\le10^9$。

输出描述

每组输出 YES 或 NO。

样例

输入

3
10 1
10
16 5
8 4 2 1 1
16 4
9 8 7 6

输出

YES
YES
NO

第一组无需切割。第二组依次保留 8、4、2,并把最后的 2 切成两个 1。第三组目标总长不等于初始长度。

思路分析

第一步:检查长度守恒。

切割只改变段数,不改变总长度,所以 $\sum a_i=L$ 是必要条件。它不是充分条件:即使总长相等,规定的对半切割方式也可能无法形成指定长度。

第二步:始终比较最大的现有绳段与最大目标。

将目标长度降序排序,用大根堆保存尚未匹配的绳段,初始只有 $L$。设堆顶长度为 $p$,当前最大目标为 $a_j$:

  • $p<a_j$:现有段都太短,继续切只会更短,无解。
  • $p>a_j$:这段比全部剩余目标都长,不能直接保留,必须继续对半切。
  • $p=a_j$:将这根绳子与目标匹配,二者从待处理集合中移除。

第三步:限制切割数量。

如果当前未匹配绳段数大于未匹配目标数,那么每段至少会留下一个最终片段,不可能再减回去,应立即失败。加入这个显式检查后,段数始终受 $n$ 限制,避免把大长度绳子无意义地细分到单位段。

正确性证明

大于最大目标的绳段不可能直接成为任何目标,切开是强制选择;小于最大目标时,任何现有段及其后代都不可能达到该目标,判无解正确。

相等时,若某个可行方案不是使用当前这段来匹配最大目标,它必然使用另一段等长绳子,因为没有更长段,而短段不能拼接。等长绳段的后续切割能力完全相同,可以交换两段在该方案中的角色,从而得到一个保留当前段的可行方案。因此直接匹配不会排除可行解。

每步操作均保持可行性判断,且总长守恒、目标全部匹配后没有剩余段,故算法输出准确。

题解代码

import heapq
import sys

input = sys.stdin.readline


def can_split(length, targets):
    if sum(targets) != length:
        return False
    targets = sorted(targets, reverse=True)
    heap = [-length]
    matched = 0
    while matched < len(targets):
        if not heap or len(heap) > len(targets) - matched:
            return False
        piece = -heapq.heappop(heap)
        target = targets[matched]
        if piece < target:
            return False
        if piece == target:
            matched += 1
        else:
            half = piece // 2
            heapq.heappush(heap, -half)
            heapq.heappush(heap, -(piece - half))
    return not heap


def solve():
    numbers = (int(token) for line in sys.stdin for token in line.split())
    t = next(numbers)
    for _ in range(t):
        length, n = next(numbers), next(numbers)
        targets = [next(numbers) for _ in range(n)]
        print('YES' if can_split(length, targets) else 'NO')


if __name__ == '__main__':
    solve()

复杂度分析

时间复杂度:单组 $O(n\log n)$。目标排序后,每次匹配移除一个目标,每次切割增加一个绳段;段数限制使切割总次数为 $O(n)$,每次堆操作为 $O(\log n)$。

空间复杂度:单组 $O(n)$,存储目标与待处理绳段堆。

易错点

奇数长度切成向下取整和向上取整两段,不能都使用整除结果。目标长度均为正,因此进入“必须切割”分支时 piece > target >= 1,不会把长度 1 切出长度 0。


第 3 题:有限免检机会下的最长检查批次

题目描述

传送带上按顺序放置 $n$ 件货物。第 $i$ 件的检查时间为 $a_i$,所需模式为 $c_i\in{0,1}$。

选择一段非空连续货物作为一个批次。开始时可以直接使用第一件货物所需模式,不额外收费;批内相邻两件的模式不同时,需要增加 $D$ 的切换时间。

每个批次最多使用一次免检机会:选其中至多 $K$ 件连续货物,将它们的检查时间改为 0。免检货物仍占据原位置,模式以及由此产生的切换时间全部保留。

总耗时不得超过预算 $B$,求最多可以选择多少件连续货物。

输入描述

第一行输入 $n,K,B,D$,第二行输入 $n$ 个检查时间,第三行输入 $n$ 个模式。

约束为:

  • $1\le n\le2\times10^5$,$1\le K\le n$。
  • $0\le B\le10^{18}$,$0\le D\le10^9$。
  • $1\le a_i\le10^9$,$c_i\in{0,1}$。

输出描述

输出满足预算的最长批次长度。

样例 1

输入

6 2 9 2
4 7 2 5 1 6
0 0 1 1 0 0

输出

4

选择前四件,免检前两件后检查时间为 $2+5=7$,模式切换一次增加 2,总耗时为 9。

样例 2

输入

3 2 3 1
8 1 8
0 1 0

输出

2

任意相邻两件可以一起免检,耗时仅为一次模式切换。三件全选时,即便免检两件,仍至少需要检查时间 8 和切换时间 2。

思路分析

第一步:固定批次,求最佳免检收益。

所有 $a_i$ 都为正,免检段越长越不吃亏。批次长度不超过 $K$ 时可以全部免检;超过 $K$ 时,最优方案一定免检其中恰好连续 $K$ 件。

使用 0 基下标。令 prefix[t] 为前 $t$ 件的检查时间总和,switches[i] 为下标 0 到 $i$ 之间的模式切换次数。对批次 $[l,r]$:

\[\operatorname{cost}(l,r)=\operatorname{prefix}[r+1]-\operatorname{prefix}[l]-\operatorname{saved}(l,r) +D\cdot(\operatorname{switches}[r]-\operatorname{switches}[l]).\]

当批次较长时,免检收益就是批内长度 $K$ 的最大子段和。预处理

\[w_s=\operatorname{prefix}[s+K]-\operatorname{prefix}[s].\]

合法免检起点恰为 $l\le s\le r-K+1$。

第二步:证明滑动窗口的单调性。

从任意批次删掉一个端点,将原来的最优免检段与缩短后的批次求交,仍得到长度不超过 $K$ 的连续免检段。保留下来的检查成本不会增加,模式切换次数也不会增加。因此缩短批次不会提高最优成本;等价地,固定左端点向右扩张时,成本不会下降。

于是右端点逐个向右加入。如果当前批次超预算,就持续右移左端点,直到重新可行。已经被移除的左端点,在右端点进一步扩张后也不可能恢复可行,无需回头。

第三步:用单调队列维护免检收益。

队列保存候选起点,按起点递增、对应 $w_s$ 严格递减排列:

  • 新右端点形成一个完整长度 $K$ 的段时,把它加入候选集合。
  • 入队前弹出所有收益不大于新段的队尾。新段更靠右、过期更晚,旧段永远不可能更优。
  • 左端点右移后,弹出起点小于左端点的队头。

队首就是最大免检收益。窗口不足 $K$ 时无需队列查询,直接免检全部检查时间。

正确性证明

单调队列只删除已离开批次的候选,或被收益不小且过期更晚的候选支配的旧段,因此始终保留当前最大免检收益,计算出的成本准确。

固定右端点时,算法从上次左端点开始收缩,所有被丢弃的窗口均已超预算;更早丢弃的左端点又由成本单调性保证仍不可能可行。停止时得到该右端点下的最长可行批次。对全部右端点取最大长度,即为全局最优。

题解代码

from collections import deque
import sys

input = sys.stdin.readline


def longest_batch(times, modes, k, budget, switch_cost):
    n = len(times)
    prefix = [0] * (n + 1)
    switches = [0] * n
    for i, value in enumerate(times):
        prefix[i + 1] = prefix[i] + value
        if i:
            switches[i] = switches[i - 1] + (modes[i] != modes[i - 1])
    window_sum = [prefix[s + k] - prefix[s] for s in range(n - k + 1)]
    candidates = deque()
    left = answer = 0
    for right in range(n):
        start = right - k + 1
        if start >= left:
            while candidates and window_sum[candidates[-1]] <= window_sum[start]:
                candidates.pop()
            candidates.append(start)
        while True:
            while candidates and candidates[0] < left:
                candidates.popleft()
            total = prefix[right + 1] - prefix[left]
            if right - left + 1 <= k:
                saved = total
            else:
                saved = window_sum[candidates[0]]
            cost = total - saved + switch_cost * (switches[right] - switches[left])
            if cost <= budget:
                break
            left += 1
        answer = max(answer, right - left + 1)
    return answer


def solve():
    n, k, budget, switch_cost = map(int, input().split())
    times = list(map(int, input().split()))
    modes = list(map(int, input().split()))
    print(longest_batch(times, modes, k, budget, switch_cost))


if __name__ == '__main__':
    solve()

复杂度分析

时间复杂度:$O(n)$。两个端点各自至多右移 $n$ 次,每个免检起点至多入队和出队各一次。

空间复杂度:$O(n)$,包括前缀数组、定长窗口和与单调队列。

易错点

  • 免检不取消模式切换费,不能把免检段从序列中物理删除。
  • $B=0$ 不意味着能选满 $K$ 件:相邻模式不同且 $D>0$ 时,即使检查费用为零也可能超预算。
  • 单件货物总能全部免检,且没有模式切换,所以题目范围内答案至少为 1,收缩循环一定能停下;不可能输出 0。

第 4 题:动态调价后的最大可选种数

题目描述

水果摊有 $n$ 种橘子,第 $i$ 种价格为 $a_i$。每天将其中一种橘子的价格永久改为新值。每次修改完成后,需要选择尽可能多的不同种类,使所选价格的最大值与最小值之差不超过 $k$。

不同种类可以价格相同,每种仍单独计数。选择不要求在商品编号上连续,只要求价格范围满足条件。

输入描述

第一行输入 $n,k$,第二行输入 $n$ 个初始价格,第三行输入修改次数 $q$。接下来 $q$ 行,每行输入 $p,x$,表示将第 $p$ 种的价格赋值为 $x$。

约束为 $1\le n\le5\times10^4$,$0\le k\le10^8$,$1\le q\le10^5$,$1\le a_i,x\le10^8$,$1\le p\le n$。

输出描述

每次修改后输出一行最多可选种数。初始状态不单独输出。

样例

输入

5 2
1 2 3 10 11
3
4 3
5 2
1 100

输出

4
5
4

三次修改后的价格分别为 [1,2,3,3,11]、[1,2,3,3,2] 和 [100,2,3,3,2]。前两次选价格区间 $[1,3]$ 内的所有种类,最后一次可以选 $[2,4]$ 内的四种。

思路分析

第一步:把选择集合转成价格窗口。

一旦选定价格区间 $[v,v+k]$,区间内所有种类都可以加入,不会破坏最大最小差条件。只需寻找容纳商品最多的窗口。

对于任意非空可行集合,令其中最低价格为 $v$,集合内所有价格都在 $[v,v+k]$ 中。因此至少有一个最优窗口以下界为某个当前价格。注意,这只说明原来选中的商品仍被包含,不是说平移后的窗口包含原窗口的所有坐标。

第二步:提前离散化所有可能价格。

读入初始价格和全部修改值,排序去重得 $v_0<\cdots<v_{m-1}$,其中 $m\le n+q$。为每个候选下界维护

\[f_i=\#\{j:v_i\le a_j\le v_i+k\}.\]

答案为所有 $f_i$ 的最大值。候选下界即使当前没有商品,也只代表一个合法价格窗口,不会让答案虚增;当前所有可能最优下界又都在候选集中。

第三步:商品的一次增删对应区间加。

价格为 $x$ 的一个种类被窗口 $[v_i,v_i+k]$ 包含,当且仅当

\[x-k\le v_i\le x.\]

这在离散数组上是一段连续下标。设 $x=v_r$,low[r] 为第一个满足 $v_i\ge v_r-k$ 的下标,则该商品恰对闭区间 [low[r], r] 的每个 $f_i$ 贡献 1。

所以一次调价只需删除旧价格的区间贡献,再加入新价格的区间贡献。初始化时可对所有商品做差分,再求前缀和得到全部 $f_i$。

第四步:维护区间加与全局最大值。

线段树节点保存 maximum 和 tag。这里仅查询全局最大值,可以使用“不下传标记”的写法:父节点标记只保存在父节点,子节点最大值不包括祖先标记,并保持

\[\operatorname{maximum}[u]=\operatorname{tag}[u]+\max(\operatorname{maximum}[2u],\operatorname{maximum}[2u+1]).\]

完全覆盖节点时同时增加其最大值和标记;部分覆盖时递归修改子节点,再按上述等式更新。根节点没有遗漏的祖先标记,所以它的最大值就是答案。若以后扩展为任意子区间查询,则需要额外累加沿途标记,不能直接照搬普通无标记查询。

正确性证明

所有满足价差限制的集合都被某个候选窗口包含,因此最大窗口计数不会小于最优集合大小;任意候选窗口内的全部商品又构成合法集合,因此不会大于最优值。二者相等。

由 $v_i\le x\le v_i+k$ 与 $x-k\le v_i\le x$ 等价可知,每次区间加减恰好改变所有且仅有受该商品影响的窗口。差分初始化准确,线段树更新保持节点最大值不变量,故每次调价后的根值恰为所需答案。

题解代码

from bisect import bisect_left
import sys

input = sys.stdin.readline


class RangeAddMax:
    def __init__(self, values):
        self.n = len(values)
        self.maximum = [0] * (4 * self.n)
        self.tag = [0] * (4 * self.n)

        def build(node, left, right):
            if left == right:
                self.maximum[node] = values[left]
                return
            middle = (left + right) // 2
            build(node * 2, left, middle)
            build(node * 2 + 1, middle + 1, right)
            self.maximum[node] = max(self.maximum[node * 2], self.maximum[node * 2 + 1])

        build(1, 0, self.n - 1)

    def add(self, query_left, query_right, delta):
        maximum, tag = self.maximum, self.tag

        def update(node, left, right):
            if query_left <= left and right <= query_right:
                maximum[node] += delta
                tag[node] += delta
                return
            middle = (left + right) // 2
            if query_left <= middle:
                update(node * 2, left, middle)
            if query_right > middle:
                update(node * 2 + 1, middle + 1, right)
            maximum[node] = tag[node] + max(maximum[node * 2], maximum[node * 2 + 1])

        update(1, 0, self.n - 1)


def price_answers(prices, operations, k):
    values = sorted(set(prices) | {value for _, value in operations})
    position = {value: i for i, value in enumerate(values)}
    low = [bisect_left(values, value - k) for value in values]
    current = [position[value] for value in prices]
    difference = [0] * (len(values) + 1)
    for index in current:
        difference[low[index]] += 1
        difference[index + 1] -= 1
    counts = []
    running = 0
    for i in range(len(values)):
        running += difference[i]
        counts.append(running)
    tree = RangeAddMax(counts)
    for p, value in operations:
        p -= 1
        old, new = current[p], position[value]
        if old != new:
            tree.add(low[old], old, -1)
            tree.add(low[new], new, 1)
            current[p] = new
        yield tree.maximum[1]


def solve():
    n, k = map(int, input().split())
    prices = list(map(int, input().split()))
    q = int(input())
    operations = [tuple(map(int, input().split())) for _ in range(q)]
    for answer in price_answers(prices, operations, k):
        print(answer)


if __name__ == '__main__':
    solve()

复杂度分析

时间复杂度:$O((n+q)\log(n+q))$。离散化与边界二分为 $O((n+q)\log(n+q))$,差分初始化和建树为 $O(n+m)$,每次修改为 $O(\log m)$。

空间复杂度:$O(n+q)$,包括离散化价格、全部修改、当前价格下标、差分数组和线段树。

易错点

$k=0$ 时统计的是同价商品的最大种数;改价是永久赋值,不是增加价格,也不是独立询问。必须将全部未来价格加入离散化;旧价与新价相同时可直接输出当前答案。


小结

  • 免费操作先看不变量,付费操作再做下界与构造证明。
  • 对半切割从最长段开始,让每一步变成匹配、强制切分或无解判断。
  • 滑动窗口必须先证明成本单调,再用单调队列维护窗口内部的最优免检段。
  • 动态价格问题可从“一个商品影响哪些窗口”反向建模,转成区间加与最大值维护。