大厂真题 / 拼多多

拼多多 5.17 暑期实习笔试真题

本场考试概述

考试时间:2026年5月17日 考试岗位:暑期实习 难度评级:困难

考点分析

  1. 第一题:枚举 / 模拟(难度简单)
  2. 第二题:哈希计数 + 枚举中点(难度中等)
  3. 第三题:双优先队列 + 动态规划(难度困难)
  4. 第四题:轮廓线 DP + 矩阵快速幂(难度困难)

建议策略

  1. 前两题尽量稳拿,实现简洁不易出错
  2. 第三题要识别”DP 转移含 $i$、$j$ 耦合项时分离变量”的技巧,再用堆加速
  3. 第四题对状态压缩 DP 和矩阵快速幂的熟悉度要求高,没把握时可以先骗 80% 的小数据分

第一题:Boss挑战

题目描述

AK 机正在挑战强大的 Boss,他有 $n$ 个技能,每个技能具有伤害值(释放一次造成的伤害)和冷却时间(释放后必须等待的时间,从释放开始计算)。

具体规则如下:

  • 战斗从时刻 $0$ 开始,持续 $T$ 秒
  • 你只能选择一个技能,在时间范围内周期性释放
  • 每次释放耗时 $1$ 秒,期间不能释放其他技能
  • 释放后必须等待冷却时间 $c_i$ 秒才能再次释放;如果 $c_i < 1$ 秒,两次释放之间的有效间隔仍为 $1$ 秒
  • 一旦选择某个技能,就必须周期性持续释放,不能中途切换

目标:选择一个技能和起始时刻(取 $0$ 最优),使得在 $T$ 秒内造成的总伤害最大。

$1 \leq n \leq 10^5$,$1 \leq T \leq 10^9$。

样例

输入

10
50
3

输出

200

技能 $1$ 个,伤害 $50$,冷却时间 $3$。从时刻 $0$ 开始释放,在 $[0, 3, 6, 9]$ 时刻各释放一次,共 $4$ 次,总伤害 $50 \times 4 = 200$。

思路分析

第一步:理解释放周期

每次释放占用 $1$ 秒,冷却时间为 $c_i$,因此两次释放的间隔为 $\max(c_i, 1)$ 秒。这是因为释放本身至少需要 $1$ 秒,当 $c_i < 1$ 时有效周期仍是 $1$。

第二步:计算释放次数

起始时刻取 $t = 0$(越早越好),在 $[0, T-1]$ 的范围内,每隔 $\text{period} = \max(c_i, 1)$ 秒释放一次,释放次数为:

\[\text{count}_i = \lfloor (T - 1) / \text{period} \rfloor + 1\]

第三步:枚举取最大值

对每个技能计算 $d_i \times \text{count}_i$,取最大值即为答案。单次遍历即可完成。

题解代码

import sys
input = sys.stdin.readline

T = int(input())
damages = list(map(int, input().split()))
cooldowns = list(map(int, input().split()))
n = len(damages)

best = 0
for i in range(n):
    # 冷却 < 1 秒时仍需 1 秒释放间隔,有效周期取 max(c, 1)
    period = max(cooldowns[i], 1)
    # 起始时间 t=0 最优,[0, T-1] 内可释放 (T-1)//period + 1 次
    count = (T - 1) // period + 1
    gain = damages[i] * count
    if gain > best:
        best = gain

print(best)

复杂度分析

  • 时间复杂度:$O(n)$,遍历每个技能做一次常数运算
  • 空间复杂度:$O(n)$,存储伤害和冷却数组

第二题:特殊三元组

题目描述

给定一个整数数组 $\text{nums}$,找出所有满足条件的特殊三元组 $(i, j, k)$,其中 $i < j < k$,满足:

\[2 \times (\text{nums}[i] - \text{nums}[j]) = \text{nums}[k]\]

由于答案可能非常大,请返回结果对 $10^9 + 7$ 取模后的值。

$1 \leq n \leq 3000$,$-10^9 \leq \text{nums}[i] \leq 10^9$。

样例

输入

1
5
8 4 2 8 4

输出

2

满足条件的三元组:$(0, 1, 3)$ 对应 $2 \times (8 - 4) = 8$;$(0, 1, 4)$ 对应… 实际验证 $2 \times (8 - 4) = 8$ 与 $\text{nums}[k]$ 匹配即可,共 $2$ 个。

思路分析

第一步:变形等式,选择枚举维度

原式 $2 \times (\text{nums}[i] - \text{nums}[j]) = \text{nums}[k]$ 等价于 $2 \times \text{nums}[i] = 2 \times \text{nums}[j] + \text{nums}[k]$,即:

\[\text{nums}[i] = \text{nums}[j] + \frac{\text{nums}[k]}{2}\]

如果暴力枚举三个下标是 $O(n^3)$,超时。降维的关键是枚举中间点 $j$,将”左侧找 $i$”和”右侧找 $k$”拆成两个独立子问题。

第二步:前缀哈希计数

固定 $j$ 后,对每个 $k > j$ 计算 $\text{target} = \text{nums}[j] + \text{nums}[k] / 2$。如果 $\text{nums}[k]$ 是奇数,$\text{target}$ 不是整数,等式无整数解,跳过。否则查询前缀哈希表中 $\text{target}$ 出现了多少次,即为有效的 $i$ 的个数。

第三步:维护哈希表的时序

从左到右扫描 $j$。对当前 $j$,先枚举所有 $k > j$ 完成查询,然后再将 $\text{nums}[j]$ 加入哈希表。这保证查询时哈希表中只包含下标严格小于 $j$ 的元素,$i < j$ 的约束自动满足。

总时间 $O(n^2)$,足以处理 $n \leq 3000$。

题解代码

import sys
input = sys.stdin.readline

MOD = 10**9 + 7


def solve_case(n, nums):
    """统计满足 2*(nums[i]-nums[j])=nums[k] 的三元组数量"""
    if n < 3:
        return 0
    # cnt[v]:当前已扫过的下标中值为 v 的个数(对应 nums[0..j-1])
    cnt = {}
    ans = 0
    # 外层枚举中点 j
    for j in range(n):
        nj = nums[j]
        # 内层枚举右端点 k,查询左侧有多少 i 满足条件
        for k in range(j + 1, n):
            nk = nums[k]
            # nums[k] 为奇数时 target 非整数,无解
            if nk % 2 == 0:
                target = nj + nk // 2
                if target in cnt:
                    ans += cnt[target]
        # 处理完 j 的所有查询后才加入前缀,保证 i < j
        cnt[nj] = cnt.get(nj, 0) + 1
    return ans % MOD


T = int(input())
out = []
for _ in range(T):
    n = int(input())
    nums = list(map(int, input().split())) if n > 0 else []
    out.append(str(solve_case(n, nums)))
print("\n".join(out))

复杂度分析

  • 时间复杂度:$O(n^2)$,外层枚举 $j$,内层枚举 $k$,哈希查询 $O(1)$
  • 空间复杂度:$O(n)$,哈希表最多存 $n$ 个不同值

第三题:自习室学习

题目描述

AK 机是个爱学习的人,最近家里在装修,他只能去附近的自习室学习。自习室有 $n$ 个房间,每个房间只有一段时间是空的。AK 机必须在空闲开始时就到达,也不必等到空闲结束就能离开。AK 机从不同的房间转移,至少需要 $2$ 分钟时间。求最长能学习多长时间。

每个房间的空闲窗口为 $[x_i, x_i + y_i)$,即从第 $x_i$ 分钟起空闲,持续 $y_i$ 分钟。

$1 \leq n \leq 10^5$,$0 \leq x_i \leq 10^9$,$1 \leq y_i \leq 10^9$。

样例

输入

2
0 50
100 50

输出

100

AK 机在第一个房间学习 $50$ 分钟,转移到第二个房间学习 $50$ 分钟,一共 $100$ 分钟。

思路分析

第一步:定义 DP 状态

按空闲开始时刻 $x_i$ 升序排序。定义 $dp[i]$ = 到达房间 $i$ 开始时($x_i$ 时刻)已累计的最长学习时间。最终答案为 $\max_i(dp[i] + y_i)$,即选 $i$ 作为最后一段并学满。

第二步:分析状态转移

考虑 $j$ 作为 $i$ 之前最后一段会话所在的房间,转场至少需要 $2$ 分钟:

  • 情况 A($j$ 学满):若 $x_j + y_j + 2 \leq x_i$,则 $j$ 有足够时间学满 $y_j$,转移为 $dp[i] = dp[j] + y_j$
  • 情况 B($j$ 提前离开):若 $x_j + 2 \leq x_i$ 但 $x_j + y_j + 2 > x_i$,则 $j$ 必须在 $x_i - 2$ 时刻离开,实际在 $j$ 学了 $x_i - x_j - 2$ 分钟,转移为 $dp[i] = dp[j] + (x_i - x_j - 2)$

第三步:分离变量加速

直接枚举所有 $j$ 是 $O(n^2)$,需要用数据结构加速。

  • 情况 A 的贡献为 $dp[j] + y_j$,只与 $j$ 有关,取前缀最大值即可
  • 情况 B 的贡献为 $(dp[j] - x_j - 2) + x_i$,其中 $x_i$ 对固定的 $i$ 是常数,只需在合法 $j$ 中求 $dp[j] - x_j - 2$ 的最大值

第四步:双堆维护

  • 堆 B:存所有满足 $x_j + 2 \leq x_i$ 的前驱,按 $dp[j] - x_j - 2$ 排序(大顶堆)
  • 堆 A:当堆 B 中的 $j$ 进一步满足 $x_j + y_j + 2 \leq x_i$ 时,升级到堆 A,按 $dp[j] + y_j$ 排序

每个 $j$ 最多入堆 B 一次、弹出后入堆 A 一次,总均摊 $O(n \log n)$。

题解代码

import sys
import heapq
input = sys.stdin.readline

N = int(input())
rooms = []
for _ in range(N):
    x, y = map(int, input().split())
    rooms.append((x, y))

# 按空闲开始时刻 x 升序排序
rooms.sort()

# dp[i]:到达房间 i 开始时已累计的最长学习时间
dp = [0] * N

# heap_A:维护"已学满前驱"的 dp[j] + y[j](负数实现大顶堆)
heap_A = []
# heap_B:维护"必须提前离开前驱"的 (dp[j] - x[j] - 2, j)
heap_B = []

j_ptr = 0  # 单调指针:跟踪哪些 j 已加入 heap_B

for i in range(N):
    x_i, y_i = rooms[i]

    # 步骤 1:把所有 x[j] + 2 <= x[i] 的 j 加入 heap_B
    while j_ptr < i and rooms[j_ptr][0] + 2 <= x_i:
        x_j, _ = rooms[j_ptr]
        heapq.heappush(heap_B, (-(dp[j_ptr] - x_j - 2), j_ptr))
        j_ptr += 1

    # 步骤 2:堆 B 堆顶若已可学满(x[j]+y[j]+2 <= x[i]),升级转入 heap_A
    while heap_B:
        _, j_idx = heap_B[0]
        x_j, y_j = rooms[j_idx]
        if x_j + y_j + 2 <= x_i:
            heapq.heappop(heap_B)
            heapq.heappush(heap_A, -(dp[j_idx] + y_j))
        else:
            break

    # 步骤 3:查询两个堆取最大转移值,0 对应"i 是第一段,无前驱"
    best = 0
    if heap_A:
        best = max(best, -heap_A[0])
    if heap_B:
        best = max(best, -heap_B[0][0] + x_i)
    dp[i] = best

# 最终答案:枚举 i 作为最后一段,学满 y[i]
answer = 0
for i in range(N):
    answer = max(answer, dp[i] + rooms[i][1])
print(answer)

复杂度分析

  • 时间复杂度:$O(n \log n)$,排序 $O(n \log n)$,每个房间至多入堆、出堆各一次
  • 空间复杂度:$O(n)$,DP 数组和堆的空间

第四题:道路修建II

题目描述

道路可以看作是一个 $M \times N$ 的网格,每个单元格需要被铺满。可以使用 $1 \times 1$ 的石砖和 $2 \times 2$ 的石砖。石块不能拆分,石块之间不可重叠,且必须完全覆盖整个网格。请计算用这两种石砖完全铺满 $M \times N$ 网格的不同修建方案数。

由于答案可能会很大,只需输出答案对 $10^9 + 7$ 取模后的值。

$1 \leq N \leq 10^{18}$,$1 \leq M \leq 5$。

样例

输入

4 2

输出

5

$4 \times 2$ 网格的合法方案共 $5$ 种:全用 $1 \times 1$、$2 \times 2$ 放最左、放中间、放最右、两个 $2 \times 2$ 并列。

思路分析

第一步:识别问题结构

$N$ 极大(可达 $10^{18}$)但 $M$ 很小($\leq 5$),自然想到按列推进。每一列只有 $2^M$ 种可能的状态($M = 5$ 时仅 $32$ 种),把列间递推关系写成矩阵乘法,再用矩阵快速幂把 $N$ 次递推压成 $O(\log N)$ 次矩阵乘。

第二步:定义列状态

用 $M$ 位二进制串表示当前列每行的”被占用”情况。第 $r$ 位为 $1$ 表示该行已被上一列延伸过来的 $2 \times 2$ 砖占据,本列不能再放东西。

第三步:列内转移(构造转移矩阵)

固定当前列的输入状态 \(\text{mask}\)(哪些行被上列占了),从上到下逐行决策:

  • 若第 $r$ 行已被占用(\(\text{mask}\) 第 $r$ 位为 $1$):跳过,处理下一行
  • 若第 $r$ 行空闲,有两个选择:
    • 放 $1 \times 1$ 砖:不影响下一列
    • 起 $2 \times 2$ 砖:要求第 $r+1$ 行也空闲且在界内,同时向右延伸到下一列(在 \(\text{new\_mask}\) 中将第 $r$ 和第 $r+1$ 位置 $1$)

遍历所有 \(\text{mask}\) 和所有合法的放置方案,构建转移矩阵 $T$,其中 \(T[\text{new\_mask}][\text{mask}]\) 表示从状态 \(\text{mask}\) 转移到 \(\text{new\_mask}\) 的方案数。

第四步:矩阵快速幂求解

初始状态为 $\text{mask} = 0$(第 $1$ 列前面没有砖伸过来),经过 $N$ 列后终态也为 $0$(第 $N+1$ 列不存在,不能有砖伸过去)。答案为 $T^N[0][0]$。

矩阵规模为 $2^M \times 2^M$($M = 5$ 时为 $32 \times 32$),每次矩阵乘法 $O(2^{3M})$,快速幂执行 $O(\log N)$ 次,总复杂度可接受。

题解代码

import sys
input = sys.stdin.readline

MOD = 10**9 + 7


def matmul(A, B, sz):
    """矩阵乘法 C = A * B,跳零项加速稀疏转移"""
    C = [[0] * sz for _ in range(sz)]
    for i in range(sz):
        Ai = A[i]
        Ci = C[i]
        for k in range(sz):
            a = Ai[k]
            if a == 0:
                continue
            Bk = B[k]
            for j in range(sz):
                Ci[j] = (Ci[j] + a * Bk[j]) % MOD
    return C


def matpow(A, p, sz):
    """矩阵快速幂 A^p:倍增法把 p 次乘法压成 O(log p) 次"""
    # 单位矩阵作为乘法初值
    R = [[1 if i == j else 0 for j in range(sz)] for i in range(sz)]
    while p > 0:
        if p & 1:
            R = matmul(R, A, sz)
        A = matmul(A, A, sz)
        p >>= 1
    return R


def build_trans(M):
    """构造列间转移矩阵 T[new_mask][mask] = 方案数"""
    sz = 1 << M
    T = [[0] * sz for _ in range(sz)]
    for mask in range(sz):
        # DFS 枚举本列从上到下的所有放置方案
        stack = [(0, 0)]  # (当前行 row, 已确定的 new_mask)
        while stack:
            row, new_mask = stack.pop()
            if row == M:
                T[new_mask][mask] += 1
                continue
            if mask & (1 << row):
                # 第 row 行已被上一列 2x2 占据,跳过
                stack.append((row + 1, new_mask))
                continue
            # 选项 1:放 1x1,下一列该行仍空闲
            stack.append((row + 1, new_mask))
            # 选项 2:起 2x2,要求 row+1 在界内且未被占用
            if row + 1 < M and not (mask & (1 << (row + 1))):
                stack.append((row + 2, new_mask | (1 << row) | (1 << (row + 1))))
    return T, sz


N, M = map(int, input().split())
T, sz = build_trans(M)
R = matpow(T, N, sz)
# 初态 mask=0,终态 mask=0
print(R[0][0] % MOD)

复杂度分析

  • 时间复杂度:$O(2^{3M} \cdot \log N)$,矩阵规模 $2^M$,快速幂 $\log N$ 次矩阵乘法。$M = 5$ 时约 $32^3 \times 60 \approx 2 \times 10^6$ 次运算
  • 空间复杂度:$O(2^{2M})$,存储转移矩阵

小结

本场拼多多笔试延续了其高难度风格,四题考点覆盖广:

题号 考点 核心技巧
T1 枚举模拟 计算有效周期,直接公式求解
T2 哈希 + 枚举 枚举中点降维,前缀计数避免重复遍历
T3 DP + 堆优化 分离变量将 $O(n^2)$ 转移优化为 $O(n \log n)$
T4 轮廓线 DP + 矩阵快速幂 状态压缩列状态,矩阵幂处理超大 $N$

关键收获

  • T2 的”枚举中点 + 前缀哈希”是三元组计数的经典降维模式,遇到 $i < j < k$ 且等式含三变量时优先考虑
  • T3 的双堆结构体现了”转移式中分离 $i$、$j$ 相关项”的思想,本质上是用堆代替线段树来维护前缀极值
  • T4 结合了轮廓线 DP 的状态定义方式和矩阵快速幂处理大规模线性递推的能力,是竞赛中处理”一维极大、另一维极小”网格问题的标准武器