DeepSeek LeetCode LCP 27. 黑盒光线反射 Java实现
·
这道题的核心是预处理 + 有序集合。
💡 解题思路
- 光线路径的循环性:在黑盒中,从任意小孔沿某方向射入的光线,最终都会回到起点并沿相同方向射出,形成一个闭合的“循环”。在所有小孔都关闭的情况下,光线会在循环中无限反射。
- 状态表示:每一个
(小孔编号, 方向)的组合,都代表了光线在循环中的一个“状态”。状态总数是2 * (2*(m+n)) - 4 = 4(m+n)-4。 - 开启小孔的作用:开启一个小孔,相当于在它所属的循环路径上,将该小孔对应的“状态位置”标记为“出口”。
- 查询的本质:
open操作就是在当前光线状态所处的循环中,查找该状态位置之后的第一个“出口”。 - 高效数据结构:使用
TreeSet维护每个循环上所有已开启小孔的位置,可以高效地完成“查找下一个”和“插入/删除”操作。
☕️ Java 代码实现
import java.util.*;
class BlackBox {
private final int n, m;
private final int totalHoles; // 小孔总数: 2 * (m + n)
private final int totalStates; // 状态总数: 4 * (m + n) - 4
// cycleId[hole][dirIndex]: 记录状态 (hole, direction) 所属的循环ID
// posInCycle[hole][dirIndex]: 记录状态在所属循环中的位置
private final int[][] cycleId;
private final int[][] posInCycle;
// 每个循环对应一个 TreeSet,存储该循环上所有已开启小孔的位置
private final List<TreeSet<Integer>> cycles;
private int cycleCount;
public BlackBox(int n, int m) {
this.n = n;
this.m = m;
this.totalHoles = 2 * (m + n);
this.totalStates = 4 * (m + n) - 4;
this.cycleId = new int[totalHoles][2];
this.posInCycle = new int[totalHoles][2];
// 初始化:-1 表示该状态尚未被分配循环
for (int i = 0; i < totalHoles; i++) {
Arrays.fill(cycleId[i], -1);
Arrays.fill(posInCycle[i], -1);
}
this.cycles = new ArrayList<>();
this.cycleCount = 0;
// 在构造函数中预处理所有循环
findAllCycles();
}
// 预处理所有光线的循环路径
private void findAllCycles() {
for (int startHole = 0; startHole < totalHoles; startHole++) {
for (int startDir : new int[]{1, -1}) {
// 拐角处的小孔只有一个合法方向
if (isCorner(startHole) && !isValidCornerDirection(startHole, startDir)) {
continue;
}
int dirIdx = dirToIndex(startDir);
// 如果这个状态已经属于某个循环,则跳过
if (cycleId[startHole][dirIdx] != -1) {
continue;
}
// 发现一个新循环
TreeSet<Integer> cycleSet = new TreeSet<>();
int currentCycleId = cycleCount++;
cycles.add(cycleSet);
int curHole = startHole;
int curDir = startDir;
int pos = 0;
// 沿着光线路径前进,直到回到起点状态
do {
// 记录当前状态
cycleId[curHole][dirToIndex(curDir)] = currentCycleId;
posInCycle[curHole][dirToIndex(curDir)] = pos;
// 计算下一个状态: 经过小孔 curHole 后,方向取反
int nextDir = -curDir;
// 计算从 (curHole, nextDir) 出发到达的下一个小孔编号
int nextHole = getNextHole(curHole, nextDir);
curHole = nextHole;
curDir = nextDir;
pos++;
} while (!(curHole == startHole && curDir == startDir));
}
}
}
// 判断小孔是否为四个拐角
private boolean isCorner(int hole) {
return hole == 0 || hole == m || hole == m + n || hole == 2 * m + n;
}
// 判断拐角处的方向是否合法
private boolean isValidCornerDirection(int hole, int dir) {
if (hole == 0) return dir == -1;
if (hole == m) return dir == 1;
if (hole == m + n) return dir == -1;
if (hole == 2 * m + n) return dir == 1;
return true;
}
// 将方向 (1 或 -1) 映射到数组索引 (0 或 1)
private int dirToIndex(int dir) {
return dir == 1 ? 0 : 1;
}
// 核心函数:给定当前小孔和下一步方向,计算到达的下一个小孔编号
private int getNextHole(int hole, int dir) {
int next;
if (dir == 1) { // 沿 y=x 方向
// 关于 (m+n) 对称
next = 2 * (m + n) - hole;
} else { // dir == -1, 沿 y=-x 方向
if (hole <= 2 * m) {
next = 2 * m - hole;
} else {
next = 2 * (2 * m + n) - hole;
}
}
// 取模运算,确保结果在 [0, totalHoles) 范围内
return (next % totalHoles + totalHoles) % totalHoles;
}
// 开启小孔并射入光线,返回射出的小孔编号
public int open(int index, int direction) {
// 1. 打开小孔:将小孔在两个方向上的状态位置,都加入到对应的循环中
for (int d : new int[]{1, -1}) {
int cycleIdx = cycleId[index][dirToIndex(d)];
if (cycleIdx != -1) {
int pos = posInCycle[index][dirToIndex(d)];
cycles.get(cycleIdx).add(pos);
}
}
// 2. 执行查询,返回光线射出的小孔
return shoot(index, direction);
}
// 关闭小孔
public void close(int index) {
// 从小孔所属的所有循环中移除其位置记录
for (int d : new int[]{1, -1}) {
int cycleIdx = cycleId[index][dirToIndex(d)];
if (cycleIdx != -1) {
int pos = posInCycle[index][dirToIndex(d)];
cycles.get(cycleIdx).remove(pos);
}
}
}
// 内部查询方法:从 (index, direction) 状态出发,寻找下一个开启的小孔
private int shoot(int index, int direction) {
int dirIdx = dirToIndex(direction);
int cycleIdx = cycleId[index][dirIdx];
int curPos = posInCycle[index][dirIdx];
TreeSet<Integer> cycleSet = cycles.get(cycleIdx);
// 在循环中寻找位置大于 curPos 的开启小孔
Integer nextPos = cycleSet.higher(curPos);
if (nextPos == null) {
// 如果没有更大的,则取循环中最小的开启小孔(绕一圈)
nextPos = cycleSet.first();
}
// 根据位置找到对应的小孔编号
return findHoleByPos(cycleIdx, nextPos);
}
// 根据循环ID和位置找到对应的小孔编号
private int findHoleByPos(int cycleIdx, int pos) {
for (int hole = 0; hole < totalHoles; hole++) {
for (int d : new int[]{1, -1}) {
if (cycleId[hole][dirToIndex(d)] == cycleIdx &&
posInCycle[hole][dirToIndex(d)] == pos) {
return hole;
}
}
}
return -1; // 正常情况下不会发生
}
}
🔑 关键点说明
findAllCycles():这是预处理的核心。它遍历所有可能的起始状态(startHole, startDir),模拟光路直到回到起点,从而发现一个完整的循环。getNextHole():这个函数是模拟光路的基础。它利用黑盒的几何对称性,通过数学计算直接得出光线经过反射后到达的下一个小孔编号。TreeSet:这是实现高效查询的关键。它维护了每个循环上当前所有开启小孔的位置(pos)。add(pos)/remove(pos):用于open和close操作。higher(curPos):用于shoot,可以 O(log N) 地找到当前位置之后的下一个出口。
- 二维数组记录状态:
cycleId和posInCycle两个二维数组,以[hole][dirIndex]为索引,记录了每个状态所属的循环和位置,是后续所有操作的基础。
⏱️ 复杂度分析
- 时间复杂度:
- 预处理 (
findAllCycles):需要遍历所有O(totalStates)个状态,因此时间复杂度为 O(n + m)。 open/close/shoot:主要操作是TreeSet的add、remove和higher,时间复杂度均为 O(log K),其中 K 是该循环上已开启的小孔数量。
- 预处理 (
- 空间复杂度:需要存储所有状态的信息和
TreeSet,整体空间复杂度为 O(n + m)。 
更多推荐



所有评论(0)