大厂真题 / 京东

京东 2026-9-26 笔试真题 - 算法岗

本场考试概述

考试时间:2026年9月26日

考试岗位:算法岗

难度评级:中等

本页收录两道具有完整题意、约束和样例的编程题。另有一道“毕业资格复核”AI Coding 工程题,但缺少完整接口契约、实体字段和配套工程,本页不将其补写成可提交题目,也不虚构接口或测试数据。

考点分析:

  • 第一题:DBSCAN 核心点、边界点与噪声点的区别;NumPy 广播和布尔矩阵。
  • 第二题:镜面反射、状态图的唯一前驱、从边界入口遍历互不相交的链。

建议策略:先完成规则明确的第一题,注意邻域包含自身与半径边界;第二题先证明轨迹结构,再写模拟,避免对每个起点重复遍历。

第 1 题:DBSCAN 点类型判定

题目描述

给定 $n$ 个 $d$ 维样本,邻域半径 eps 和最小样本数 min_samples。不需要求聚类簇编号,只需输出每个点的类型:核心点为 2,边界点为 1,噪声点为 0。

点 $i$ 的邻域包含所有与它的欧氏距离不超过 eps 的点,包含点自身。若邻域点数不少于 min_samples,该点是核心点;否则,如果它位于至少一个核心点的邻域内,就是边界点;剩余点为噪声点。边界点不能像核心点一样继续扩张邻域。

输入一行 JSON,字段 X 为样本矩阵,eps 为正浮点数,min_samples 为不小于 $1$ 的整数。约束为 $1<n<20$、$0<d<10$。

输出一行长度为 $n$ 的 JSON 整数数组,元素间保留空格。数值计算只允许使用 NumPy、pandas 或 scikit-learn;下面使用 NumPy,标准库负责输入输出与 JSON 解析。

样例

输入

{"X":[[0,0],[0,0.1],[0.1,0],[5,5]],"eps":0.2,"min_samples":3}

输出

[2, 2, 2, 0]

思路分析

第一步:把邻域关系表示成矩阵。

类型判定只依赖两点是否相邻,无须执行完整的 DBSCAN 聚类。计算

\[D_{ij}=\sum_{k=1}^{d}(X_{ik}-X_{jk})^2,\]

再令 $A_{ij}$ 表示 $D_{ij}\le\mathrm{eps}^2$。比较平方距离可避免开平方,不能把 <= 改成 <,也不应擅自引入容差改变题目规则。矩阵对角线为真,自身已经计算在邻域内,不要额外加一。

第二步:先确定全部核心点。

矩阵每行的真值数量就是对应点的邻域大小。把行和与 min_samples 比较,得到核心点掩码。样例的前三个点互相相邻,每个都有三个邻居,因此均为核心点。

第三步:只检查核心点对应的列。

对于非核心点,如果它所在行在某个核心点列上为真,就应输出边界点。欧氏距离对称,因此“它靠近某个核心点”与“它落在核心点邻域里”等价。必须先判核心,再判边界,不能把同时邻近其他核心点的核心点降级。

当没有核心点时,取出的矩阵有零列,NumPy 的 any(axis=1) 为每一行返回假,因此自然全部输出噪声点。当 min_samples=1 时,所有点都因自身而成为核心点。最终用 tolist() 转为 Python 整数列表,再由 json.dumps 生成规定格式。

题解代码

import sys
import json
import numpy as np


def classify_points(x, eps, min_samples):
    diff = x[:, None, :] - x[None, :, :]
    adjacent = np.sum(diff * diff, axis=2) <= eps * eps
    core = adjacent.sum(axis=1) >= min_samples
    near_core = adjacent[:, core].any(axis=1)
    types = np.zeros(len(x), dtype=int)
    types[near_core & ~core] = 1
    types[core] = 2
    return types.tolist()


def solve():
    data = json.loads(sys.stdin.readline())
    x = np.asarray(data["X"], dtype=float)
    result = classify_points(x, float(data["eps"]), int(data["min_samples"]))
    print(json.dumps(result))


if __name__ == "__main__":
    solve()

复杂度分析

时间复杂度:$O(n^2d)$,枚举所有点对并计算各维度差值,后续布尔运算不超过 $O(n^2)$。

空间复杂度:$O(n^2d)$,主要为广播得到的三维差值张量。

第 2 题:镜子实验室

题目描述

实验室是 $n$ 行 $m$ 列的网格,每格为 .、/ 或 \。从每个格子分别向上、右、下、左发射光线,共有 $4nm$ 种起始条件。

每一步,光线先在当前格子反射,再沿新方向移动一格:

  • .:方向不变。
  • /:上与右互换,下与左互换。
  • \:上与左互换,下与右互换。

离开网格即成功逃离;光线也可能永远在网格中循环。起始位置或起始方向不同就视为不同光线,求最终能逃离的光线数量。

第一行是测试组数 $T$,满足 $1\le T\le100$。每组先输入 $n,m$,再输入 $n$ 行长度为 $m$ 的网格。满足 $n,m\ge1$、$nm\le10^6$,所有测试组的 $nm$ 之和不超过 $10^6$。每组输出一个整数。

样例

输入

2
2 3
...
...
2 2
/\
\/

输出

24
8

思路分析

第一步:把格子和方向一起作为状态。

一个状态是“位于某格、尚未处理该格镜子的运动方向”。这恰好对应一种起始光线,共 $4nm$ 个状态。每个状态的后继唯一;逐束模拟虽然直接,但会反复走过相同路径,最坏达到平方级工作量。

第二步:证明前驱也至多一个。

如果光线以方向 $d$ 进入某格,那么前一格只能是沿 $d$ 反方向退一格的位置。在前一格中,空地及两种镜面的方向映射都是双射,所以反射前方向也唯一。若前一格位于网格外,该状态没有内部前驱。

因此状态图中,每个点的入度和出度都不超过一。有限图的每个连通部分只能是链或环:环上的光线永不逃离;链的末端一定离开网格。链不可能汇入环,否则汇入点会有两个前驱。

第三步:从没有前驱的状态开始走。

所有链的起点恰好是边界入口:第一行向下、最后一行向上、第一列向右、最后一列向左。枚举这些入口,沿反射规则一直走到出口。每访问一个状态,就找到一种能够逃离的起始条件,累加访问次数即可。

各条链不共享状态,也不会进入环,所以不需要访问标记或判环集合。注意统计的是链上的状态数,不是入口数量。单行、单列或角上的同一个格子会出现多个入口,但方向不同,对应不同状态,不重复计数。

第四步:用一圈哨兵简化边界。

将方向依次编码为 $0,1,2,3$(上、右、下、左),反射用查表完成。把网格存为一维字节数组,四周补值为 3 的哨兵;走到哨兵就结束。实际格子的编码为 0、1、2。入口用生成器逐个产生,不额外保存整个入口列表。

样例第一组没有镜子,所有状态均逃离。第二组既有外侧链,也有内部循环;只遍历边界链便会得到答案 8,不会陷入内部循环。

题解代码

import sys


TURN = ((0, 1, 2, 3), (1, 0, 3, 2), (3, 2, 1, 0))
TRANS = bytes.maketrans(b"./\\", bytes((0, 1, 2)))


def boundary_starts(n, m, width):
    for col in range(1, m + 1):
        yield width + col, 2
        yield n * width + col, 0
    for row in range(1, n + 1):
        yield row * width + 1, 1
        yield row * width + m, 3


def count_escape(n, m, rows):
    width = m + 2
    grid = bytearray([3]) * width
    for row in rows:
        grid.append(3)
        grid.extend(row.translate(TRANS))
        grid.append(3)
    grid.extend(bytearray([3]) * width)
    steps = (-width, 1, width, -1)
    total = 0
    for pos, direction in boundary_starts(n, m, width):
        while grid[pos] != 3:
            direction = TURN[grid[pos]][direction]
            pos += steps[direction]
            total += 1
    return total


def solve():
    input = sys.stdin.buffer.readline
    t = int(input())
    for _ in range(t):
        n, m = map(int, input().split())
        rows = (input().strip() for _ in range(n))
        print(count_escape(n, m, rows))


if __name__ == "__main__":
    solve()

复杂度分析

时间复杂度:$O(nm)$,各条链互不相交,总访问次数不超过 $4nm$,建立网格也为线性开销。

空间复杂度:$O(nm)$,保存带边框的网格;入口和输入行均逐个产生。

小结

  • DBSCAN 点类型判定不需要构造完整聚类;邻域矩阵、核心掩码和邻近核心的判断已经足够。
  • 镜面网格不能只看“后继唯一”,还要利用“前驱唯一”,才能证明从边界入口出发的路径互不相交且不会进入环。
  • 工程题只有业务概述而缺少完整契约时,不应自行填补字段或接口,把猜测包装成完整真题。