下面是 LeetCode 850 题「矩形面积 II」的 Java 解法。该解法使用扫描线 + 离散化 + 线段树,时间复杂度 O(n log n),空间复杂度 O(n)。

import java.util.*;

class Solution {
    private static final int MOD = 1_000_000_007;

    public int rectangleArea(int[][] rectangles) {
        // 1. 收集所有 y 坐标,用于离散化
        List<Integer> yList = new ArrayList<>();
        for (int[] rect : rectangles) {
            yList.add(rect[1]);
            yList.add(rect[3]);
        }
        Collections.sort(yList);
        // 去重
        List<Integer> ys = new ArrayList<>();
        for (int i = 0; i < yList.size(); i++) {
            if (i == 0 || !yList.get(i).equals(yList.get(i - 1))) {
                ys.add(yList.get(i));
            }
        }
        int m = ys.size();
        // 离散化映射:y 值 -> 下标
        Map<Integer, Integer> yToIdx = new HashMap<>();
        for (int i = 0; i < m; i++) {
            yToIdx.put(ys.get(i), i);
        }

        // 2. 构建事件
        List<Event> events = new ArrayList<>();
        for (int[] rect : rectangles) {
            int x1 = rect[0], y1 = rect[1], x2 = rect[2], y2 = rect[3];
            // 左边界:+1,右边界:-1
            events.add(new Event(x1, y1, y2, 1));
            events.add(new Event(x2, y1, y2, -1));
        }
        // 按 x 排序
        events.sort(Comparator.comparingInt(e -> e.x));

        // 3. 线段树初始化,区间个数 = m-1
        SegTree segTree = new SegTree(ys);

        // 4. 扫描线
        long ans = 0;
        int lastX = events.get(0).x;
        int i = 0;
        int n = events.size();
        while (i < n) {
            int curX = events.get(i).x;
            // 计算从上个 x 到当前 x 的面积
            ans = (ans + (long) (curX - lastX) * segTree.queryTotal()) % MOD;
            lastX = curX;

            // 处理所有 x == curX 的事件
            while (i < n && events.get(i).x == curX) {
                Event e = events.get(i);
                int y1Idx = yToIdx.get(e.y1);
                int y2Idx = yToIdx.get(e.y2);
                // 更新区间 [y1Idx, y2Idx-1] 对应的线段树
                segTree.update(y1Idx, y2Idx - 1, e.type);
                i++;
            }
        }
        return (int) ans;
    }

    // 事件类
    class Event {
        int x, y1, y2, type; // type = 1 添加,-1 删除
        Event(int x, int y1, int y2, int type) {
            this.x = x;
            this.y1 = y1;
            this.y2 = y2;
            this.type = type;
        }
    }

    // 线段树,维护区间覆盖次数和覆盖总长度
    class SegTree {
        private int[] cover;   // 覆盖次数
        private long[] length; // 当前覆盖长度(实际长度)
        private List<Integer> ys; // 离散化后的 y 坐标列表

        public SegTree(List<Integer> ys) {
            this.ys = ys;
            int size = ys.size() - 1; // 区间个数
            cover = new int[4 * size];
            length = new long[4 * size];
        }

        // 更新区间 [l, r] 增加 val(val = 1 或 -1)
        public void update(int l, int r, int val) {
            update(1, 0, ys.size() - 2, l, r, val);
        }

        private void update(int node, int nodeL, int nodeR, int l, int r, int val) {
            if (l <= nodeL && nodeR <= r) {
                cover[node] += val;
                // 根据覆盖情况重新计算当前节点的长度
                if (cover[node] > 0) {
                    // 完全覆盖,长度就是区间对应的实际长度
                    length[node] = ys.get(nodeR + 1) - ys.get(nodeL);
                } else {
                    // 没有覆盖,长度等于子节点长度之和(如果是叶子则为 0)
                    if (nodeL == nodeR) {
                        length[node] = 0;
                    } else {
                        length[node] = length[node * 2] + length[node * 2 + 1];
                    }
                }
                return;
            }
            int mid = (nodeL + nodeR) / 2;
            if (l <= mid) update(node * 2, nodeL, mid, l, r, val);
            if (r > mid) update(node * 2 + 1, mid + 1, nodeR, l, r, val);
            // 回溯时更新当前节点长度(前提是 cover[node] == 0)
            if (cover[node] == 0) {
                length[node] = length[node * 2] + length[node * 2 + 1];
            }
        }

        // 查询当前被覆盖的总长度
        public long queryTotal() {
            return length[1];
        }
    }
}

核心思路

  1. 离散化 y 坐标:由于 y 的范围很大(10^9),无法直接开数组,因此将所有矩形的 y1、y2 收集起来排序去重,得到 m 个不同的 y 值,形成 m-1 个小区间。
  2. 扫描线:将每个矩形拆成左边界(+1)和右边界(-1)事件,按 x 坐标排序。
  3. 线段树维护 y 轴覆盖长度:
    · 线段树的叶子节点对应一个小区间 [ys[i], ys[i+1]),节点存储当前被覆盖的次数(cover)和该区间内被覆盖的实际长度(len)。
    · 当某个区间的 cover > 0 时,len 等于区间实际长度;否则 len 等于左右子节点 len 之和。
    · 更新操作为区间加 1 或减 1,递归到完全覆盖的节点后直接修改 cover 并更新 len,无需下推懒标记(因为 cover 本身就是标记)。
  4. 面积累加:扫描时,每到一个新的 x,先计算上一个 x 到当前 x 之间的面积 = 当前覆盖总长度 × x 间隔,然后处理该 x 上的所有事件(更新线段树)。最后将结果模 10^9+7 返回。

注意点

· 事件处理顺序:先计算面积,再处理当前 x 的事件,避免将当前 x 上的矩形误计入上一段间隔。
· 线段树更新时,区间为 [y1Idx, y2Idx-1],因为离散化下标 i 对应的是 [ys[i], ys[i+1])。
· 使用 long 类型防止中间结果溢出,最后取模。

该方法可以高效处理最多 200 个矩形(题目数据范围),是官方题解的标准做法。

Logo

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

更多推荐