大厂真题 / 拼多多

拼多多 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$ 的位置。它们一定被本次操作真正改变,因此必须全部位于操作区间内。

记最左、最右的差异位置为 leftright。题目保证操作存在,而且操作至少改变一个实例,所以差异位置一定非空。目标状态也随之确定:

\[v = B_{\text{left}}\]

合法区间必须覆盖完整的 [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$ 个请求有两种处理方式:

  1. 丢弃它,从 $f_{j-1}(i-1)$ 转移
  2. 保留它,并让最后一个批次包含下标区间 $[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] 为满足长度差约束的最小下标,则完整转移为:

\[f_j(i)=\min\left(f_{j-1}(i-1),\ 1+\min_{\max(\text{first\_valid}[i-1],\ i-C)\leq p<i}f_j(p)\right)\]

对于 $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)$。层与层之间只保留 previouscurrent 两个数组,不需要保存完整的 $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
  • 第四题把边操作视为异或方程,利用基环树只有一个自由环的性质,只需构造一个特解并翻转整环即可得到全部方案