大厂真题 / 美团

美团 4.18 笔试真题 - 研发岗

本场考试概述

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

考点分析

  1. 第一题:排序 + 模拟(难度简单)
  2. 第二题:排序 + 二分查找(难度中等)
  3. 第三题:贪心 + 树状数组(难度困难)

建议策略

  1. 第一题排序标记删除即可,注意处理值相同时优先删左边的规则,快速拿满分
  2. 第二题对每个点预排距离后二分,注意用距离平方避免浮点误差
  3. 第三题是经典的”字典序最大 LCS”问题,核心是将两个排列的 LCS 转化为 LIS,再用 BIT + 贪心逐位确定答案

第一题:清除残留数据

题目描述

给定一个长度为 n 的序列,需要删除其中最小的 m 个元素。如果有多个元素值相同,优先删除最左边的。删除后,输出剩余元素按原始顺序排列的结果。

多组测试数据,第一行输入 T。每组数据第一行给出 n 和 m,第二行给出数组 a。保证所有测试数据的 n 之和不超过 200000。

样例

输入

3
5 2
3 1 4 1 5
3 0
1 2 3
5 4
5 4 3 2 1

输出

3 4 5
1 2 3
5

思路分析

第一步:理解删除规则

我们需要从序列中找出最小的 m 个元素并删除它们。关键约束是:当多个元素值相同时,优先删除位于左边的那个。这就要求我们在排序比较时,同值的元素按下标从小到大排,这样取前 m 个就自然满足”优先删左边”的规则。

第二步:确定哪些位置被删除

将所有元素按 (值, 下标) 排序,取排序后的前 m 个元素,记录它们的下标为”被删除”。

第三步:按原顺序输出剩余元素

遍历原数组,跳过被标记为删除的位置,将剩余元素按顺序输出。

以样例第一组为例:数组 [3, 1, 4, 1, 5],m=2。按 (值, 下标) 排序后为 (1,1), (1,3), (3,0), (4,2), (5,4)。取前 2 个:下标 1 和 3 被删除。原数组中保留下标 0, 2, 4 对应的 3, 4, 5。

题解代码

import sys
input = sys.stdin.readline

def solve():
    n, m = map(int, input().split())
    a = list(map(int, input().split()))
    if m == 0:
        print(*a)
        return
    # 按 (值, 下标) 排序,取前 m 个的下标标记为删除
    indices = sorted(range(n), key=lambda i: (a[i], i))
    removed = set(indices[:m])
    # 按原顺序输出未被删除的元素
    res = [a[i] for i in range(n) if i not in removed]
    print(*res)

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

复杂度分析

时间复杂度:O(n log n),排序为主要开销。 空间复杂度:O(n),存储排序后的下标和删除集合。


第二题:二维坐标系

题目描述

平面上有 n 个点。对于每对点 (i, j)(i != j),定义 c[i][j] 为:以点 i 为圆心、dist(i, j) 为半径的圆内(含圆上)的其他点的数量(不包括 i 和 j 本身)。即 c[i][j] 等于满足 k != i 且 k != j 且 dist(i, k) <= dist(i, j) 的点 k 的数量。定义 c[i][i] = 0。

多组测试数据,第一行输入 T。每组数据第一行给出 n,接下来 n 行每行两个整数表示坐标。保证所有测试数据的 n 之和不超过 5000。

样例

输入

1
4
0 0
1 0
0 1
1 1

输出

0 1 1 2
1 0 2 1
1 2 0 1
2 1 1 0

思路分析

第一步:理解 c[i][j] 的含义

c[i][j] 本质上是:以点 i 为圆心画一个刚好经过点 j 的圆,这个圆内部和边界上还有多少个其他点(不算 i 和 j)。

直觉上,如果 j 离 i 很远,那圆就很大,里面包含的其他点就多。

第二步:为什么排序 + 二分?

对于固定的点 i,c[i][j] 只依赖于 dist(i, j) 的相对大小。如果我们预先将所有其他点按到 i 的距离排好序,那么对于任意 j,只需要知道”距离 <= dist(i, j) 的点有多少个”,这正好是排序后的前缀计数问题,可以用二分查找 O(log n) 解决。

具体地:对于点 i,计算它到其余所有点的距离平方(用 int64 避免浮点),排序后对每个 j 二分查找 upper_bound(dist_sq(i, j)),得到”距离 <= dist(i, j) 的点个数”,再减 1(排除 j 本身)。

第三步:为什么用距离平方?

坐标范围到 10^6,距离平方最大约 (2 * 10^6)^2 = 4 * 10^12,在 int64 范围内。使用距离平方可以完全避免浮点运算和精度问题,且不影响大小比较。

第四步:算法流程

  1. 对每个点 i,计算到其余 n-1 个点的距离平方,存入列表并排序
  2. 对每对 (i, j),用 bisect_right 在 i 的排序列表中查找 dist_sq(i, j) 的上界位置,该位置就是”距离 <= dist(i, j)”的点数(不含 i),减去 1(排除 j)就是 c[i][j]

以样例为例:4 个点构成单位正方形。对于点 0=(0,0):到点 1 距离平方=1,到点 2 距离平方=1,到点 3 距离平方=2。排序后为 [1, 1, 2]。c[0][1]:bisect_right([1,1,2], 1) = 2,减 1 = 1。c[0][3]:bisect_right([1,1,2], 2) = 3,减 1 = 2。

题解代码

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

def solve():
    n = int(input())
    pts = []
    for _ in range(n):
        x, y = map(int, input().split())
        pts.append((x, y))

    # 对每个点 i,预计算到其他所有点的距离平方并排序
    dist_sorted = [[] for _ in range(n)]
    for i in range(n):
        dists = []
        xi, yi = pts[i]
        for j in range(n):
            if j == i:
                continue
            dx = pts[j][0] - xi
            dy = pts[j][1] - yi
            dists.append(dx * dx + dy * dy)
        dists.sort()
        dist_sorted[i] = dists

    # 计算 c[i][j]
    out = []
    for i in range(n):
        row = []
        xi, yi = pts[i]
        for j in range(n):
            if j == i:
                row.append(0)
            else:
                dx = pts[j][0] - xi
                dy = pts[j][1] - yi
                d2 = dx * dx + dy * dy
                # 在排序列表中找 <= d2 的个数,减 1 排除 j
                cnt = bisect_right(dist_sorted[i], d2) - 1
                row.append(cnt)
        out.append(" ".join(map(str, row)))
    print("\n".join(out))

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

复杂度分析

时间复杂度:O(n^2 log n),对每个点排序 O(n log n),共 n 个点;查询共 n^2 次,每次 O(log n)。 空间复杂度:O(n^2),存储每个点的距离排序数组。


第三题:最长公共子序列3

题目描述

给定两个长度为 n 的排列 p 和 q(即 1 到 n 的全排列),求它们的字典序最大的最长公共子序列(LCS)。输出 LCS 的长度和具体序列。

多组测试数据,第一行输入 T。每组数据第一行给出 n,接下来两行分别给出排列 p 和 q。保证所有测试数据的 n 之和不超过 200000。

样例

输入

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

输出

2
3 4
1
5
4
6 7 8 5

思路分析

第一步:LCS 转化为 LIS

两个排列的 LCS 有一个经典转化:构造序列 c,其中 c[i] 表示 p[i] 在 q 中出现的位置(即 q 的逆排列在 p[i] 处的值)。那么 p 和 q 的 LCS 等价于 c 的最长递增子序列(LIS)。

为什么?p 和 q 的公共子序列要求选出的元素在 p 中下标递增,同时在 q 中下标也递增。如果我们用 c[i] = pos_q(p[i])(p[i] 在 q 中的位置),那么选择一组下标 i1 < i2 < … < ik 使得 c[i1] < c[i2] < … < c[ik],就恰好对应 p 和 q 的一个公共子序列。因此 LCS 长度 = LIS(c) 的长度。

第二步:求字典序最大的 LIS

现在问题变成:在序列 c 上找字典序最大的 LIS。这里”字典序最大”是指 LIS 对应的原始值(p[i] 的值)字典序最大,而不是 c 值字典序最大。

关键思路:贪心地从左到右逐位确定结果。对于结果的第 k 位,我们想选最大的 p[i] 值,同时满足:

  1. c[i] 严格大于已选的上一个 c 值(保证递增性)
  2. 从位置 i 开始(含 i),剩余的后缀中能形成的 LIS 长度足够填满剩余位数

条件 2 需要预计算:对每个位置 i,从 i 开始的”后缀 LIS 长度”(即以 c[i] 为起点、只考虑 i 及之后的元素能形成的最长递增子序列长度)。

第三步:用 BIT 计算后缀 LIS 长度

从右到左扫描,维护一个树状数组(BIT),BIT 的下标对应 c 值,存储的是以 >= 该 c 值开头的 LIS 最大长度。

对于位置 i,suffix_lis[i] = 1 + BIT 查询 (c[i]+1, n) 的最大值(即在 i 之后、c 值严格大于 c[i] 的位置中最长递增子序列长度的最大值)。然后将 suffix_lis[i] 更新到 BIT 的位置 c[i]。

由于 BIT 通常用于前缀查询,而我们需要后缀最大值查询(查 [c[i]+1, n] 的最大值),可以做坐标翻转:令 c’[i] = n + 1 - c[i],则查询 [c[i]+1, n] 变成查询前缀 [1, c’[i]-1] 的最大值,标准 BIT 即可处理。

第四步:贪心选择——按 p[i] 值从大到小扫描

LIS 的总长度 L = max(suffix_lis[i])。我们需要逐步选出 L 个元素。

贪心策略:

  1. 将所有位置按 p[i] 值从大到小排序(值相同不可能,因为是排列)
  2. 维护 last_c = -1(上一个选中元素的 c 值)和 remain = L(还需选多少个)
  3. 按 p 值从大到小遍历候选位置 i,检查是否满足:
    • c[i] > last_c(递增约束)
    • suffix_lis[i] >= remain(后缀够长)
  4. 满足则选中 i,更新 last_c = c[i],remain -= 1
  5. 但还需注意位置约束:选中 i 后,后续只能选位置 > i 的元素

第五步:处理位置约束的细节

上述贪心有一个微妙之处:选中位置 i 后,后续候选必须在 i 之后(位置更大)。但我们按 p 值降序排列候选,位置是乱序的。

更精确的做法:

  1. 按 p[i] 值降序排列所有位置
  2. 对于结果的每一位,从当前候选中找第一个满足 c[i] > last_c、位置 > last_pos 且 suffix_lis[i] >= remain 的位置
  3. 但直接暴力扫描可能退化为 O(n^2)

优化方法:对候选按 p 值降序排列后,维护 last_pos(上一个选中位置)。对于每一轮选择,扫描候选列表,跳过位置 <= last_pos 或 c[i] <= last_c 的,找到第一个满足 suffix_lis[i] >= remain 的。由于每个位置最多被扫描和跳过一次(一旦被跳过或选中就不再考虑),总复杂度仍为 O(n log n)。

以样例第三组为例:p = [9,2,6,7,3,8,1,4,5],q = [6,2,7,9,4,8,3,5,1]。构造 c:p[0]=9 在 q 中位置 4,p[1]=2 在 q 中位置 2,p[2]=6 在 q 中位置 1,p[3]=7 在 q 中位置 3,p[4]=3 在 q 中位置 7,p[5]=8 在 q 中位置 6,p[6]=1 在 q 中位置 9,p[7]=4 在 q 中位置 5,p[8]=5 在 q 中位置 8。c = [4,2,1,3,7,6,9,5,8]。LIS(c) 长度为 4(如子序列 1,3,6,8 或 1,3,5,8 等)。字典序最大的对应原值为 6,7,8,5。

题解代码

import sys
input = sys.stdin.readline

def solve():
    n = int(input())
    p = list(map(int, input().split()))
    q = list(map(int, input().split()))

    # pos_in_q[v] = v 在 q 中的位置(0-indexed)
    pos_in_q = [0] * (n + 1)
    for i in range(n):
        pos_in_q[q[i]] = i

    # c[i] = p[i] 在 q 中的位置
    c = [pos_in_q[p[i]] for i in range(n)]

    # 用 BIT 求后缀 LIS 长度
    # suffix_lis[i] = 以 c[i] 开头、只考虑位置 >= i 的最长递增子序列长度
    # BIT 维护前缀最大值(坐标翻转后)
    bit = [0] * (n + 2)

    def query(pos):
        """查询 [1, pos] 的最大值"""
        res = 0
        while pos > 0:
            res = max(res, bit[pos])
            pos -= pos & (-pos)
        return res

    def update(pos, val):
        """更新位置 pos 的值(取 max)"""
        while pos <= n:
            bit[pos] = max(bit[pos], val)
            pos += pos & (-pos)

    suffix_lis = [0] * n

    # 从右到左扫描,坐标翻转:c'[i] = n - c[i]
    # 查询 c[i]+1..n-1 的最大值 = 查询翻转后 1..n-1-c[i] 的前缀最大值
    for i in range(n - 1, -1, -1):
        # 翻转坐标:原始 c[i] 在 [0, n-1],翻转为 n - c[i](范围 [1, n])
        flipped = n - c[i]  # [1, n]
        # 查询原始坐标 > c[i] 即翻转坐标 < flipped,即前缀 [1, flipped-1]
        best = query(flipped - 1) if flipped > 1 else 0
        suffix_lis[i] = best + 1
        # 更新翻转坐标 flipped 位置
        update(flipped, suffix_lis[i])

    total_lis = max(suffix_lis)

    # 贪心选取字典序最大的 LIS
    # 按 p[i] 值降序排列所有位置
    order = sorted(range(n), key=lambda i: -p[i])

    result = []
    last_c = -1
    last_pos = -1
    remain = total_lis
    ptr = 0  # 指向 order 中的当前位置

    while remain > 0:
        for idx in range(ptr, n):
            i = order[idx]
            # 位置必须在 last_pos 之后,c 值必须大于 last_c
            if i <= last_pos or c[i] <= last_c:
                continue
            if suffix_lis[i] >= remain:
                result.append(p[i])
                last_c = c[i]
                last_pos = i
                remain -= 1
                ptr = idx + 1
                break

    print(total_lis)
    print(*result)

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

复杂度分析

时间复杂度:O(n log n)。BIT 更新和查询各 O(log n),共 n 次;排序 O(n log n);贪心选取过程中每个位置最多被访问一次(指针 ptr 单调前进),总计 O(n)。 空间复杂度:O(n),存储 c 数组、suffix_lis 数组、BIT 和排序数组。


小结

  • 第一题是排序模拟题,核心是按 (值, 下标) 排序来处理”同值优先删左边”的规则,标记删除后按原顺序输出,是一道送分题
  • 第二题利用”距离平方”避免浮点,对每个点预排距离后二分查找,本质是将”圆内点计数”转化为”有序数组前缀计数”,是排序 + 二分的经典应用
  • 第三题是本场最难的题目,将两个排列的 LCS 转化为 LIS 是关键的第一步转化,随后用 BIT 计算后缀 LIS 长度,再用贪心按值降序逐位确定答案,需要对 LIS 的结构有深入理解