大厂真题 / 美团

美团 3.28 笔试真题 - 研发岗

本场考试概述

考试时间:2026年3月28日 考试岗位:研发岗 难度评级:中等偏难

考点分析

  1. 第一题:贪心排序(难度中等)
  2. 第二题:序列DP(难度中等)
  3. 第三题:分治 + RMQ(难度困难)

建议策略

  1. 第一题贪心思路清晰后代码量很小,快速拿下
  2. 第二题序列DP需要仔细定义状态,注意前缀最值优化
  3. 第三题分治+短边枚举是竞赛经典技巧,难度较高但套路固定

第一题:风不吹雨

题目描述

给定一个长度为 n 的数组。你有两种操作:

  • 操作一:选择一个元素 x,将其变为 floor(x/2)。全局最多使用 a 次。
  • 操作二:选择一个元素 x,将其变为 x-k。全局最多使用 b 次。

每个位置可以同时使用两种操作。请最小化数组的总和。

样例

输入

1
3 1 1 3
5 1 7

输出

6

解释:对 7 使用操作一得到 3,对任意元素使用操作二减去 3,总和 = 5 + 1 + 3 - 3 = 6。

思路分析

这道题的关键在于分别理解两种操作的本质。

操作二的本质:固定收益。无论对哪个元素使用操作二,总和都恰好减少 k。所以 b 次操作二带来的总收益恒为 bk,与选择哪个元素无关。这意味着操作二不需要任何策略,直接从总和中扣除 bk 即可。

操作一的本质:可变收益。对元素 x 使用操作一,总和减少 x - floor(x/2) = ceil(x/2)。这个收益取决于 x 的大小——x 越大,收益越高。

两种操作的先后顺序:如果对同一个元素先操作一再操作二,收益 = ceil(x/2) + k;先操作二再操作一,收益 = k + ceil((x-k)/2)。由于 ceil(x/2) >= ceil((x-k)/2),先操作一更优。但因为操作二的收益固定为 k,不管先后,操作二的总贡献始终是 b*k。而操作一应该在原始值上计算收益——因为我们要最大化操作一的收益,用原始值(更大)算 ceil(x/2) 更大。

贪心策略

  1. 计算每个元素使用操作一的收益 ceil(x/2) = (x+1)//2
  2. 对收益降序排序,取前 a 个
  3. 总和减去这些收益,再减去 b*k

为什么排序贪心是正确的? 因为操作一的 a 次使用是独立的——每次操作一只影响一个元素,且收益只取决于该元素的原始值。我们要从 n 个独立的收益中选 a 个最大的,这正是排序贪心的经典场景。

时间复杂度:O(n log n),瓶颈在排序。 空间复杂度:O(n)。

import sys
input = sys.stdin.readline

def solve():
    n, a, b, k = map(int, input().split())
    arr = list(map(int, input().split()))
    total = sum(arr)
    gains = sorted(((x + 1) // 2 for x in arr), reverse=True)
    total -= sum(gains[:a])
    total -= b * k
    return total

T = int(input())
for _ in range(T):
    print(solve())

第二题:交替子序列

题目描述

给定长度为 n 的数组 a,选择一个子序列 p1 < p2 < … < pm,最大化:

F = a[p1] - a[p2] + a[p3] - a[p4] + …

即奇数位置(第1、3、5…个)取正号,偶数位置(第2、4、6…个)取负号。

样例

输入

2
3
1 2 3
5
-1 2 -3 4 -5

输出

3
14

解释

  • 样例一:选择子序列 [3],F = 3。
  • 样例二:选择子序列 [2, -3, 4, -5, 4] 中的 [2, -3, 4, -5, 4]?实际上选 [-1, -3, 4, -5, 4] 并不对。正确选法:选择下标 2,3,4,5 即 [2, -3, 4, -5],F = 2-(-3)+4-(-5) = 2+3+4+5 = 14。

思路分析

这是一道经典的序列DP问题,核心在于正确定义状态。

状态定义

  • odd_i:以元素 a[i] 结尾、且 a[i] 处于子序列的奇数位置(取正号)时的最大 F 值
  • even_i:以元素 a[i] 结尾、且 a[i] 处于子序列的偶数位置(取负号)时的最大 F 值

状态转移

对于 odd_i(a[i] 取正号):

  • a[i] 可以作为子序列的第一个元素,此时 F = a[i]
  • a[i] 前面可以接一个偶数位置结尾的子序列,此时 F = (前面最优的 even 值) + a[i]
  • 因此 odd_i = max(a[i], prefix_max_even + a[i])

对于 even_i(a[i] 取负号):

  • a[i] 必须前面有一个奇数位置结尾的子序列(不能单独作为偶数位置开始)
  • even_i = prefix_max_odd - a[i]

为什么用前缀最值优化? 朴素转移需要遍历 i 之前所有 j,即 odd_i = max(a[i], max_{j<i}(even_j) + a[i])。注意到 max_{j<i}(even_j) 就是一个前缀最大值,可以在遍历过程中维护,将 O(n^2) 优化到 O(n)。

答案:所有 odd_ieven_i 中的最大值。子序列可以在任何位置结束——如果最后一个元素在奇数位(正号),取 odd_i;如果在偶数位(负号),取 even_i

直觉理解:这道题本质上是在数组中选若干对”高-低”差值累加。奇数位选大的值,偶数位选小的值(最好是负数),这样正负号叠加后收益最大。DP 的妙处在于它自动处理了所有可能的子序列长度和位置组合。

时间复杂度:O(n)。 空间复杂度:O(1),只需维护两个前缀最值变量。

import sys
input = sys.stdin.readline

def solve():
    n = int(input())
    a = list(map(int, input().split()))
    NEG_INF = float('-inf')
    prefix_max_odd = NEG_INF
    prefix_max_even = NEG_INF
    ans = 0
    for i in range(n):
        odd_i = a[i] if prefix_max_even == NEG_INF else max(a[i], prefix_max_even + a[i])
        even_i = NEG_INF
        if prefix_max_odd != NEG_INF:
            even_i = prefix_max_odd - a[i]
        prefix_max_odd = max(prefix_max_odd, odd_i)
        prefix_max_even = max(prefix_max_even, even_i)
        if odd_i != NEG_INF:
            ans = max(ans, odd_i)
        if even_i != NEG_INF:
            ans = max(ans, even_i)
    return ans

T = int(input())
for _ in range(T):
    print(solve())

第三题:AK机的区间

题目描述

给定长度为 n 的数组 a,统计满足以下条件的区间 [l, r](l < r)的个数:

max(a[l], a[l+1], …, a[r]) < a[l] + a[r]

即区间内的最大值严格小于左右端点之和。

样例

输入

5
1 2 3 4 5

输出

10

解释:所有 C(5,2) = 10 个区间都满足条件。以 [1,5] 为例,max = 5 = a[5],而 a[1]+a[5] = 1+5 = 6 > 5,满足。

思路分析

暴力枚举所有区间是 O(n^2),加上区间最大值查询至少 O(n^2 log n),对于大数据不可行。本题需要利用分治的思想,结合”短边枚举”技巧达到 O(n log^2 n)。

核心观察:对于区间 [l, r],设 m 是该区间最大值的位置。条件变为 a[m] < a[l] + a[r],即 a[l] > a[m] - a[r](或 a[r] > a[m] - a[l])。

分治框架

  1. 在当前区间 [L, R] 中找到最大值位置 m(用稀疏表 RMQ 预处理,O(1) 查询)
  2. 所有 [l, r] 满足 L <= l <= m <= r <= R 的合法对,分为三类:
    • l = m 或 r = m:即一个端点恰好是最大值位置。此时条件简化为 a[m] < a[m] + a[另一端],即 a[另一端] > 0。由于题目中数组元素为正整数,这些对全部合法,共 (m-L) + (R-m) 对
    • l < m 且 r > m:最大值 a[m] 夹在 l 和 r 之间,需要 a[l] + a[r] > a[m]
  3. 递归处理子区间 [L, m-1] 和 [m+1, R]

短边枚举 + 二分查找

对于第三类(l < m, r > m),如何高效计数?

  • 取 m 两侧较短的一边进行枚举(设为短边),较长的一边排序后二分
  • 例如左侧 [L, m-1] 较短,则枚举每个 l,条件 a[r] > a[m] - a[l],在右侧排序数组中二分查找满足条件的 r 的个数
  • 反之若右侧较短,则枚举 r,在左侧排序数组中二分

为什么短边枚举保证复杂度? 这是一个经典结论:每次选短边枚举,每个元素在所有递归层中被枚举的总次数是 O(log n)。直觉是:一个元素只有在它所在的那一侧是短边时才会被枚举,而每次被枚举后,下一层递归区间至少缩小一半,所以每个元素最多被枚举 O(log n) 次。加上每次枚举时的 O(log n) 二分查找,总复杂度 O(n log^2 n)。

RMQ 预处理:使用稀疏表(Sparse Table),预处理 O(n log n),查询 O(1)。这是区间最大值的标准工具。预处理 sp[j][i] 表示从位置 i 开始、长度为 2^j 的区间内最大值的下标。查询时将区间拆成两个可重叠的 2^k 长度区间取最大值。

实现细节

  • 用栈模拟递归避免栈溢出
  • 短边排序的数组在每层递归中重新构建,不影响总复杂度
  • 注意 bisect_right 找的是严格大于 a[m] - a[i] 的位置,对应的元素个数即 len(sorted_arr) - pos

时间复杂度:O(n log^2 n)。 空间复杂度:O(n log n),稀疏表的空间。

import sys
input = sys.stdin.readline
from bisect import bisect_right

def solve():
    n = int(input())
    v = list(map(int, input().split()))
    LOG = [0] * (n + 1)
    for i in range(2, n + 1):
        LOG[i] = LOG[i // 2] + 1
    K = LOG[n] + 1
    sp = [[0] * n for _ in range(K)]
    for i in range(n):
        sp[0][i] = i
    for j in range(1, K):
        for i in range(n - (1 << j) + 1):
            li = sp[j-1][i]
            ri = sp[j-1][i + (1 << (j-1))]
            sp[j][i] = li if v[li] >= v[ri] else ri

    def qmax(l, r):
        k = LOG[r - l + 1]
        li = sp[k][l]
        ri = sp[k][r - (1 << k) + 1]
        return li if v[li] >= v[ri] else ri

    ans = 0
    stack = [(0, n - 1)]
    while stack:
        l, r = stack.pop()
        if l >= r:
            continue
        m = qmax(l, r)
        ans += (m - l) + (r - m)
        if m - l <= r - m:
            rv = sorted(v[m+1:r+1])
            for i in range(l, m):
                pos = bisect_right(rv, v[m] - v[i])
                ans += len(rv) - pos
        else:
            lv = sorted(v[l:m])
            for j in range(m+1, r+1):
                pos = bisect_right(lv, v[m] - v[j])
                ans += len(lv) - pos
        stack.append((l, m - 1))
        stack.append((m + 1, r))
    print(ans)

solve()

小结

  • 第一题贪心排序,关键洞察是操作二的收益固定、操作一的收益与元素大小正相关,排序取前 a 大即可
  • 第二题序列DP,定义奇偶位置两种状态并用前缀最值优化转移,将 O(n^2) 降至 O(n)
  • 第三题分治+RMQ是竞赛经典组合,短边枚举技巧将复杂度控制在 O(n log^2 n),建议作为高频模板掌握