大厂真题 / 拼多多

拼多多 2026-9-13 笔试真题 - 技术岗

本场考试概述

考试时间:2026年9月13日

考试岗位:技术岗

难度评级:中等偏难(按算法建模与实现难度评估)

考点分析

  • 第一题:半开区间、扫描线与峰值时长(简单)。
  • 第二题:整数二分答案、前缀和与前缀最小值(中等)。
  • 第三题:严格递增转非递减、值域离散化与动态规划(中等)。
  • 第四题:KMP 自动机、乘积图、Dijkstra 最短路计数(困难)。

建议策略:先完成扫描线,再用整数判定处理平均值;第三题先证明候选值范围再写 DP;最后为图上路径补充字符串匹配状态。全文包含四道题、七份输入输出样例,其中第二题的一份输入包含三组测试数据。


第 1 题:配送员最少人数与满员时长

题目描述

配送站有若干任务,每个任务必须在指定的开始时刻至结束时刻,由同一名配送员全程执行,不能提前或延后。一名配送员不能同时承担多个任务,但任务结束时可以立即接手另一个任务。

求完成全部任务所需的最少配送员人数,以及采用这一人数时,所有配送员同时忙碌的累计时长。满足条件的时段可能不连续,需要将长度相加。任务对应半开区间 [s,e),相同的任务记录也分别占用人手。

输入描述

第一行一个整数 $n$,满足 $1\le n\le 200000$。接下来 $n$ 行,每行两个整数 $s,e$,满足 $0\le s<e\le 10^9$。任务以任意顺序给出。

输出描述

输出两个整数:最少人数,以及所有人同时执行任务的累计时长。

样例 1

输入

5
1 4
2 3
3 5
4 6
6 7

输出

2 3

样例 2

输入

5
4 7
1 3
8 9
0 2
5 6

输出

2 2

第一份样例在 [2,5) 始终有两个任务同时执行,累计时长为 3。第二份样例的满员区间为 [1,2)[5,6),累计时长为 2。

思路分析

第一步:把人员安排转成区间覆盖。 如果某时刻有 $k$ 个任务同时执行,就至少需要 $k$ 人。反过来,按开始时间分配任务,只要有空闲配送员就复用;若最大重叠数为 $k$ 却找不到空闲者,新任务加入后就会形成 $k+1$ 个重叠,产生矛盾。因此答案人数恰好是最大重叠数。

第二步:只处理事件时刻。 时间上界很大,不能逐时刻开数组。在开始位置记 +1,结束位置记 -1,用字典聚合同一时间的净变化,再对所有时间排序。同刻事件一起处理,天然符合结束后立即接手的规则,不会制造虚假的瞬时峰值。

第三步:累计峰值对应的长度。 更新时刻 time 的净变化后,覆盖数在下一个事件之前保持不变。如果发现更大的覆盖数,更新峰值,并把累计时长重置为当前段长度;如果等于现有峰值,就累加长度。最后一个事件之后已经没有任务,不必统计。

正确性证明

上述下界与分配论证说明,最大重叠数等于最少人数。每个相邻事件区间内部没有开始或结束事件,扫描得到的覆盖数正是该整段的任务数。这些区间不重不漏地划分了全部任务活动时间。维护过程中,一旦峰值增大就丢弃旧峰值的时长,峰值相等时才累加,因此结束时保存的恰好是全局最大重叠数及其总持续时间。

题解代码

import sys
input = sys.stdin.readline


def min_couriers(intervals):
    events = {}
    for start, end in intervals:
        events[start] = events.get(start, 0) + 1
        events[end] = events.get(end, 0) - 1
    times = sorted(events)
    current = peak = duration = 0
    for i in range(len(times) - 1):
        time = times[i]
        current += events[time]
        length = times[i + 1] - time
        if current > peak:
            peak, duration = current, length
        elif current == peak:
            duration += length
    return peak, duration


def solve():
    n = int(input())
    intervals = [tuple(map(int, input().split())) for _ in range(n)]
    print(*min_couriers(intervals))


if __name__ == '__main__':
    solve()

复杂度分析

时间复杂度:$O(n\log n)$,最多有 $2n$ 个事件时刻,排序主导;字典操作按均摊常数计。

空间复杂度:$O(n)$,用于任务、事件字典和排序后的时间数组。

易错点

  • 不要把区间当成两端闭合,同刻交接不需要额外配送员。
  • 统计的是达到全局峰值的总时长,不是最长连续满员时段。
  • 出现更大峰值时,必须重置此前累积的时长。

第 2 题:连收模式最大平均日产量

题目描述

采摘季有 $n$ 天,第 $i$ 天的净产量为整数 $a_i$,可以为负数,代表运输及损耗造成净亏损。需要选择一段连续日子 $[l,r]$ 开启连收模式,长度至少为 $L$。

该区间的报表平均日产量定义为:

\[\left\lfloor\frac{\sum_{i=l}^{r}a_i}{r-l+1}\right\rfloor.\]

向下取整指不大于原数的最大整数,例如 $\lfloor-8/3\rfloor=-3$。求所有合法区间中这一指标的最大值。

输入描述

第一行整数 $T$,满足 $1\le T\le10$。每组数据两行:

  • 第一行两个整数 $n,L$,满足 $1\le L\le n\le 2\times10^5$。
  • 第二行 $n$ 个整数 $a_i$,满足 $-10^9\le a_i\le10^9$。

所有测试组的 $n$ 之和不超过 $2\times10^5$。

输出描述

每组数据输出一行一个整数,表示最大的向下取整平均日产量。

样例 1

输入

3
5 3
3 -4 5 2 -1
3 2
-5 -3 -8
4 4
7 -2 -3 7

输出

2
-4
2

第一组可取 [5,2,-1],平均值为 2;第二组取 [-5,-3],平均值为 -4;第三组必须选全部四天,总和为 9,向下取整后为 2。

思路分析

第一步:把最优化改成判定。 枚举所有区间需要平方级时间。对于整数 $x$,有 $\lfloor avg\rfloor\ge x$ 当且仅当 $avg\ge x$。于是可以二分“是否存在长度至少为 $L$、平均值至少为 $x$ 的区间”。目标越低越容易满足,判定具有单调性。

第二步:把平均值改成区间和。 将每项减去 $x$,条件等价于区间内变换后的和非负。定义前缀和 $S_0=0$,$S_i=\sum_{j=1}^{i}(a_j-x)$。固定右端点 $r$,合法左边界对应的前缀下标为 $0\le j\le r-L$。因此只需检查:

\[S_r-\min_{0\le j\le r-L}S_j\ge0.\]

第三步:线性维护最小前缀。 从 $r=L$ 开始扫描,每次先把 $S_{r-L}$ 加入前缀最小值,再判断当前右端点。有任意一个满足即可返回真;无需存每个区间,也无需浮点除法。

第四步:二分最大可行整数。 下界取数组最小值,上界取最大值,使用偏上的中点。可行时保留中点为新下界,否则上界减至中点减一,直到边界重合。

正确性证明

对每个整数候选值,减去该值后区间和非负与原平均值达到该值等价。前缀最小值涵盖且仅涵盖所有长度符合要求的左边界,因此判定函数当且仅当存在合法区间时返回真。可行性随候选值增大只会由真变假,且答案一定在最小元素与最大元素之间;上取中点的二分保留最大可行答案并严格缩小范围,最终输出正确结果。

题解代码

import sys
input = sys.stdin.readline


def max_average(a, limit):
    n = len(a)

    def feasible(target):
        prefix = [0] * (n + 1)
        for i, value in enumerate(a, 1):
            prefix[i] = prefix[i - 1] + value - target
        smallest = 0
        for right in range(limit, n + 1):
            smallest = min(smallest, prefix[right - limit])
            if prefix[right] >= smallest:
                return True
        return False

    low, high = min(a), max(a)
    while low < high:
        mid = (low + high + 1) // 2
        if feasible(mid):
            low = mid
        else:
            high = mid - 1
    return low


def solve():
    tests = int(input())
    for _ in range(tests):
        n, limit = map(int, input().split())
        a = list(map(int, input().split()))
        print(max_average(a, limit))


if __name__ == '__main__':
    solve()

复杂度分析

时间复杂度:每组 $O(n\log(V+1))$,其中 $V=\max a-\min a+1$;包含全相等数组时的线性读入成本。

空间复杂度:$O(n)$,保存原数组和当前二分轮次的前缀和。

易错点

  • 长度是至少 $L$,不能只枚举恰好 $L$ 的滑动窗口。
  • 负数的向下取整不是向零截断;代码避免除法与浮点误差。
  • 先加入下标 right-limit,否则会漏掉长度恰好为 $L$ 的区间。
  • L=n、全负数及全相等数组都必须正常处理。

第 3 题:魔法水晶严格递增最少操作

题目描述

有 $n$ 颗从左到右排列的水晶,初始能量为 $a_1,\ldots,a_n$。每次操作可以任选一颗水晶,使其能量增加 1 或减少 1。求使能量序列严格递增所需的最少操作次数。调整后的能量没有额外范围限制。

输入描述

第一行整数 $n$,满足 $1\le n\le2000$。第二行 $n$ 个整数 $a_i$,满足 $\lvert a_i\rvert\le10^9$。

输出描述

输出一个整数,表示最少操作次数。

样例 1

输入

5
1 1 1 1 1

输出

6

样例 2

输入

4
1 2 3 4

输出

0

第一份样例可调整为 [-1,0,1,2,3],各位置代价为 2,1,0,1,2,总计 6。第二份样例已经严格递增,无需操作。

思路分析

第一步:消去严格递增中的位置差。 设最终序列为 $c_i$,令 $b_i=a_i-i$、$d_i=c_i-i$,下标从 1 开始。整数的严格递增等价于相邻至少增加 1,因此 $c_i<c_{i+1}$ 恰好等价于 $d_i\le d_{i+1}$。同时 $\lvert a_i-c_i\rvert=\lvert b_i-d_i\rvert$,总操作代价不变。

第二步:将无限值域压成有限候选。 目标变成绝对偏差和最小的非递减序列。存在一个最优解,其中每个值都取自 $b$。把最优序列划分成取值相等的连续块;若某块取值不是任意 $b_i$,其代价在遇到某个 $b_i$ 前是线性函数,可以向不增加代价的方向移动,直到遇到某个数据值或邻块。遇到邻块就合并再考虑。如此消除非候选块,最终取值全部落在数据值上。因此可用 values=sorted(set(b)),候选数记为 $K\le n$。

第三步:按最后一个取值做 DP。 令 $f_i(j)$ 表示处理前 $i$ 项、当前项等于 values[j] 的最小代价。前一项只能选择不大于当前候选的值:

\[f_i(j)=\min_{0\le k\le j}f_{i-1}(k)+\lvert b_i-\mathrm{values}[j]\rvert.\]

初始化虚拟空序列 $f_0(j)=0$。最终答案为 $\min_j f_n(j)$。

第四步:前缀最小值与滚动数组。 若每次都枚举 $k$,需要立方级时间。按 $j$ 升序扫描,用 best 维护上一行的前缀最小值,每个状态只做常数次计算。原地覆盖前先把旧 dp[j] 纳入 best,不需要额外二维表。

正确性证明

位置变换在原严格递增序列与新非递减序列之间建立保代价双射;候选值论证保证离散化不丢失最优解。对 DP 按已处理项数归纳:当前候选为第 $j$ 个值时,所有合法前驱恰好对应下标不超过 $j$ 的状态,转移枚举这些前驱的最小代价并加入当前修改量,既不漏解也不引入非法解。前缀最小值只是等价加速,故最终最小值就是原问题答案。

题解代码

import sys
input = sys.stdin.readline


def min_operations(a):
    b = [value - i for i, value in enumerate(a, 1)]
    values = sorted(set(b))
    dp = [0] * len(values)
    for value in b:
        best = dp[0]
        for j, candidate in enumerate(values):
            # 必须先读取上一行的 dp[j],再覆盖当前位置。
            best = min(best, dp[j])
            dp[j] = best + abs(value - candidate)
    return min(dp)


def solve():
    n = int(input())
    a = list(map(int, input().split()))
    print(min_operations(a))


if __name__ == '__main__':
    solve()

复杂度分析

时间复杂度:$O(n\log n+nK)=O(n^2)$,其中 $K$ 是变换后不同元素个数。

空间复杂度:$O(n)$,保存变换数组、候选数组与滚动 DP。

易错点

  • 离散化的是 a[i]-i,不是原数组;直接对原值做非递减 DP 会改变题意。
  • 递增针对最终整数序列,不应限制修改只能增加。
  • 原地更新时必须先读取旧状态。只把当前相邻逆序对修好并不保证全局代价最小。
  • 操作次数可能超过 32 位整数;Python 整数可以直接保存。

第 4 题:合规路线最小费用与方案数

题目描述

物流网络有 $n$ 个仓库和 $m$ 条单向道路。每条道路有正整数费用,以及 ABC 中的一个标签。从仓库 $s$ 出发到仓库 $t$,按行驶顺序连接道路标签,得到路线的标签串。

给定非空禁用串 $P$,标签串不得包含连续子串 $P$。车辆可以重复经过仓库和道路,每次经过都支付费用。求合规路线的最小总费用及达到该费用的路线数,路线数对 $1000000007$ 取模。

道路按输入顺序编号。道路编号序列不同就视为不同路线,即使端点、费用和标签全部相同的重边也分别计数。当 $s=t$ 时允许空路线,费用为 0、标签串为空。

输入描述

第一行四个整数 $n,m,s,t$,满足 $1\le n\le5000$、$0\le m\le20000$、$1\le s,t\le n$。

第二行是仅含 ABC 的字符串 $P$,满足 $1\le\lvert P\rvert\le50$。

接下来 $m$ 行,每行 u v w ch,表示从 $u$ 到 $v$ 的有向道路,费用为 $w$、标签为 ch。满足 $1\le u,v\le n$、$1\le w\le10^9$,允许重边与自环。

部分数据约束:20% 的数据满足 $n\le10$、$m\le20$、$\lvert P\rvert\le4$,且每条边起点编号小于终点编号;另有 30% 的数据中禁用串只有一个字符;其余 50% 无额外限制。

输出描述

输出最小总费用与最优路线数(取模后),用空格分隔。如果不存在合规路线,输出 -1 0

样例 1

输入

4 5 1 4
AB
1 2 1 A
2 4 1 B
1 3 1 B
3 4 2 A
1 4 3 C

输出

3 2

样例 2

输入

2 1 1 2
A
1 2 7 A

输出

-1 0

第一份样例中,1→2→4 的费用虽为 2,但标签串 AB 被禁止;1→3→4 的标签串 BA 与直达路线的 C 均合法,费用都是 3,所以输出 3 2。第二份样例的唯一道路标签被禁止,无解。

思路分析

第一步:普通最短路缺少历史信息。 到达同一仓库的两条路线,后缀可能不同,再走相同标签的道路时,可能一条合法、另一条触发禁串。只用仓库作为状态会错误合并路线,而保存全部标签串又没有有限的长度上界。

第二步:用 KMP 后缀匹配长度概括历史。 令 $q=\lvert P\rvert$,记录已走标签串的后缀与 $P$ 的前缀相同的最大长度 $j$,合法状态满足 $0\le j<q$。利用 KMP 失配数组构建 go[j][c]:字符匹配时前进一位,否则复用较短匹配前缀的转移。若新长度等于 $q$,说明刚刚形成完整禁串,该边不能走。三个字符的转移可以按状态从小到大在线性时间填表。

第三步:在乘积图上跑 Dijkstra。 状态为 (u,j),其中 u 为仓库,j 为匹配长度。原图边 u→v 在标签转移合法时,生成到 (v,go[j][c]) 的同权状态边。只保存原图,扫描邻接表时即时计算状态边;状态编码为 u*q+j,数组保存距离与条数。

第四步:最短距离和条数同步维护。(s,0) 出发,距离设为 0、条数设为 1。发现更短距离时,覆盖距离与条数并入堆;距离相等时,只将前驱条数加到已有条数中,不重复入堆。过期的堆条目直接跳过。

所有边权严格为正,因此一条最短路径的前驱距离严格小于后继距离。状态正式出堆时,所有能为它贡献最短路计数的前驱都已经处理完成,计数可以安全地向后传播。

第五步:汇总所有终点匹配状态。 终点不要求匹配长度为 0,在所有 (t,j) 中取最小距离,并将达到该距离的条数相加。不可达时返回 -1 0。当起终点相同时,正权保证空路线是唯一的零费用路线。

正确性证明

KMP 状态精确记录判定下一字符是否形成禁串所需的最长前缀后缀信息,因此每条被保留的状态边恰好对应一次合法延伸。从 (s,0) 出发,给定道路编号序列会唯一确定自动机状态序列,合法路线与状态路径一一对应,费用保持不变,重边也不会被合并。

状态图边权均为正,Dijkstra 得到所有可达状态的最短距离。按最短距离递增归纳,每个状态出堆时所有最短前驱都已贡献完毕;更短距离覆盖、等长距离相加恰好统计全部最短路线。最后对具有全局最小距离的终点状态求和,不重不漏地得到目标答案。

题解代码

import sys
import heapq
input = sys.stdin.readline
MOD = 1000000007


def automaton(pattern):
    size = len(pattern)
    fail = [0] * size
    for i in range(1, size):
        j = fail[i - 1]
        while j and pattern[i] != pattern[j]:
            j = fail[j - 1]
        if pattern[i] == pattern[j]:
            j += 1
        fail[i] = j
    go = [[0] * 3 for _ in range(size)]
    for j in range(size):
        for c, char in enumerate('ABC'):
            if pattern[j] == char:
                go[j][c] = j + 1
            elif j:
                go[j][c] = go[fail[j - 1]][c]
    return go


def shortest_routes(n, edges, source, target, pattern):
    size = len(pattern)
    go = automaton(pattern)
    graph = [[] for _ in range(n)]
    for u, v, weight, char in edges:
        graph[u].append((v, weight, ord(char) - ord('A')))
    infinity = float('inf')
    dist = [infinity] * (n * size)
    count = [0] * (n * size)
    start = source * size
    dist[start], count[start] = 0, 1
    heap = [(0, start)]
    while heap:
        distance, state = heapq.heappop(heap)
        if distance != dist[state]:
            continue
        u, matched = divmod(state, size)
        for v, weight, char in graph[u]:
            nxt = go[matched][char]
            if nxt == size:
                continue
            dest = v * size + nxt
            candidate = distance + weight
            if candidate < dist[dest]:
                dist[dest] = candidate
                count[dest] = count[state]
                heapq.heappush(heap, (candidate, dest))
            elif candidate == dist[dest]:
                count[dest] = (count[dest] + count[state]) % MOD
    answer = min(dist[target * size:(target + 1) * size])
    if answer == infinity:
        return -1, 0
    ways = sum(count[target * size + j] for j in range(size)
               if dist[target * size + j] == answer) % MOD
    return answer, ways


def solve():
    n, m, source, target = map(int, input().split())
    pattern = input().strip()
    edges = []
    for _ in range(m):
        u, v, weight, char = input().split()
        edges.append((int(u) - 1, int(v) - 1, int(weight), char))
    print(*shortest_routes(n, edges, source - 1, target - 1, pattern))


if __name__ == '__main__':
    solve()

复杂度分析

令 $q=\lvert P\rvert$,状态数 $N=nq$,候选状态边数 $M=mq$。

时间复杂度:$O(n+m+q+N+M\log(M+2))$。自动机的字母表大小固定为 3;最多扫描 $M$ 条状态边,lazy heap 的成功松弛与堆操作次数为 $O(M+1)$。用 $M+2$ 避免无边图的对数边界问题。

空间复杂度:$O(n+m+q+N+M)$。原图与状态数组分别占 $O(n+m)$、$O(N)$,但 Python heapq 不支持原地 decrease-key,过期条目仍可能滞留,堆最坏占 $O(M+1)$;不能因为未显式建状态边就忽略这一项。

易错点

  • 禁止的是连续子串,不是子序列。匹配失败要沿失配链回退,而不是简单清零。
  • 必须允许重复仓库和道路,不要在原图层面设置永久访问标记。
  • 重边代表不同道路编号,应保留并分别累计;自环也应保留。
  • 相等距离只加条数,不再次入堆,否则可能重复向后传播。
  • 正权是计数正确性的关键;本题方法不能未经修改套到含零权环的题目。
  • 不要只输出 (t,0) 的结果,也不要漏掉 m=0s=t 的情形。

小结

  • 第一题:资源最少需求等于同时进行任务的峰值,时长需要按事件间隔累计。
  • 第二题:平均值优化可通过减去候选答案化成区间和判定,再维护合法左端点的最小前缀。
  • 第三题:减去位置下标将严格递增转成非递减,绝对偏差的分段线性性质使值域离散化成立。
  • 第四题:图上约束依赖路径历史时,可以把有限自动机并入状态;计数还需检查边权与堆实现的前提。