这道题的核心是预处理 + 有序集合

💡 解题思路

  • 光线路径的循环性:在黑盒中,从任意小孔沿某方向射入的光线,最终都会回到起点并沿相同方向射出,形成一个闭合的“循环”。在所有小孔都关闭的情况下,光线会在循环中无限反射。
  • 状态表示:每一个 (小孔编号, 方向) 的组合,都代表了光线在循环中的一个“状态”。状态总数是 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):用于 openclose 操作。
    • higher(curPos):用于 shoot,可以 O(log N) 地找到当前位置之后的下一个出口。
  • 二维数组记录状态cycleIdposInCycle 两个二维数组,以 [hole][dirIndex] 为索引,记录了每个状态所属的循环和位置,是后续所有操作的基础。

⏱️ 复杂度分析

  • 时间复杂度
    • 预处理 (findAllCycles):需要遍历所有 O(totalStates) 个状态,因此时间复杂度为 O(n + m)
    • open / close / shoot:主要操作是 TreeSetaddremovehigher,时间复杂度均为 O(log K),其中 K 是该循环上已开启的小孔数量。
  • 空间复杂度:需要存储所有状态的信息和 TreeSet,整体空间复杂度为 O(n + m)
  • 在这里插入图片描述
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐