DSA DSA In Depth
0%
lesson sweep-linebentley-ottmannrectangle-union

Sweep-Line Paradigm: Bentley-Ottmann Segment Intersection & Area of Union of Rectangles

7 min read Module 6.4: Computational Geometry, Number Theory, Randomized & Approximation Algorithms Updated Oct 23, 2026
📄 Cheat Sheet
Line Segment Intersection — Sweep-Line Algorithm (Robert Sedgewick)
Osiris Salazar (Princeton Algorithms) Line Segment Intersection — Sweep-Line Algorithm (Robert Sedgewick) Watch on YouTube ↗

Shows how sweeping an imaginary vertical line across 2D space reduces static 2D geometric intersection and measure problems into dynamic 1D data structure updates.

0:00 / --:--

1. Formal Invariant — Sweep-Line Status & No-Pushdown Coverage Segment Tree

Invariant 1: Bentley-Ottmann Local Adjacency Property

Suppose two segments $S_a$ and $S_b$ intersect at $x = x^*$. Immediately to the left of $x^*$ (after processing all events with $x < x^*$), $S_a$ and $S_b$ must be immediately adjacent in the vertical sweep-line status BST! Therefore, checking only newly adjacent pairs upon segment insertion, deletion, or intersection swap detects all $K$ intersections in $O((N + K) \log N)$ time.

Invariant 2: Klee's Measure Segment Tree (No Lazy Propagation Required!)

When sweeping vertical edges of $N$ rectangles $(x_1, +1, [y_1, y_2])$ and $(x_2, -1, [y_1, y_2])$, every $-1$ deletion removes an interval that was previously inserted as $+1$. Each Segment Tree node $u$ stores:

  • $\text{cnt}[u] \ge 0$: how many active rectangles completely cover node $u$'s canonical $y$-interval.
  • $\text{len}[u]$: total covered length inside $u$'s subtree: if $\text{cnt}[u] > 0$, then $\text{len}[u] = Y[r+1] - Y[l]$; otherwise $\text{len}[u] = \text{len}[2u] + \text{len}[2u+1]$.

Because we only ever query the global root $\text{len}[1]$, we never need lazy pushdown!

2. Sweep-Line Architecture for Area of Union of Rectangles

flowchart LR Rects["N Axis-Aligned Rectangles
[x1, x2] x [y1, y2]"] --> Events["Create 2N Vertical Events:
(x1, +1, y1, y2) and (x2, -1, y1, y2)
+ Coordinate Compress Y"] Events --> Sweep["Sweep Events i = 0..2N-1:
Area += segtree.root_len() * (x_i - x_{i-1})"] Sweep --> Update["Update Segment Tree on [y1, y2] with +1 / -1
Recalculate len[u] in O(log N)"] Update --> Sweep

3. Closest Pair of Points via Sweep-Line in $O(N \log N)$

Another classic sweep-line application finds the **Closest Pair of Points** among $N$ points without divide-and-conquer: sort points by $x$, maintain a sliding window of points with $x_i - x_j < d$ inside a std::set ordered by $y$, and for each point $P_i$, query the std::set range $[y_i - d, y_i + d]$. By a geometric packing argument, at most **6 points** can exist inside the $d \times 2d$ bounding box with pairwise distance $\ge d$, so the inner loop executes at most 6 iterations per point!

4. Hardware & Memory Access Reality

Coordinate Compression + Flat Array Segment Tree

In Klee's Measure Problem, $y$-coordinates can span $[-10^9, 10^9]$, but at most $2N$ distinct $y$-coordinates exist. Sorting and deduplicating them into a flat array Y[0..M-1] maps every elementary interval $[Y[k], Y[k+1]]$ to leaf $k$ of a static $4M$-element array Segment Tree, running $10\times$ faster than pointer-based dynamic trees.

5. Complexity Matrix

Andrew Monotone Chain Convex Hull & Sweep-Line Events
O(N log N) · CCW Cross Product

Trace 2D cross-product orientation tests cross(O, A, B) = (Ax-Ox)(By-Oy) - (Ay-Oy)(Bx-Ox) to prune clockwise interior vertices and detect sweep-line segment intersections.

Step 1 / 8
Custom Input:
Timeline

Invariant & State

cross(P0, P1, P2) < 0 (Clockwise / Right Turn): P1 lies strictly inside the lower hull and is popped from the stack.
P0 P1 (Pop!) P2 P3 P4 P5
Inspected Triple
P0 → P1 → P2
Cross Product
cross < 0 (Right Turn)
Stack Action
Pop P1, Push P2
[Step 1] Right turn detected at P1; popped P1 so lower hull edge connects P0 → P2 directly.
Sweep-Line Problem Time Complexity Space Complexity Sweep-Line Status Structure
Any Two Segments Intersect (Shamos-Hoey) O(N log N) O(N) Balanced BST (std::set) ordered by $y(x)$
All $K$ Segment Intersections (Bentley-Ottmann) O((N + K) log N) O(N + K) Balanced BST + Priority Queue of Events
Area / Perimeter of Union of $N$ Rectangles O(N log N) O(N) Coordinate-Compressed Coverage Segment Tree
2D Closest Pair of Points O(N log N) O(N) Sliding-Window std::set ordered by $y$

6. Production-Grade C++20 Implementation (Area of Union of Rectangles)

#include <vector>
#include <cstdint>
#include <algorithm>

struct Rect {
    int64_t x1, y1, x2, y2; // [x1, x2] x [y1, y2] with x1 < x2, y1 < y2
};

int64_t rectangle_union_area(const std::vector<Rect>& rects) {
    if (rects.empty()) return 0;

    struct Event {
        int64_t x;
        int type; // +1 for entering left edge, -1 for leaving right edge
        int64_t y1, y2;
        bool operator<(const Event& o) const {
            return x != o.x ? x < o.x : type > o.type;
        }
    };

    std::vector<Event> events;
    std::vector<int64_t> coords_y;
    events.reserve(rects.size() * 2);
    coords_y.reserve(rects.size() * 2);

    for (const auto& r : rects) {
        if (r.x1 >= r.x2 || r.y1 >= r.y2) continue;
        events.push_back({r.x1, +1, r.y1, r.y2});
        events.push_back({r.x2, -1, r.y1, r.y2});
        coords_y.push_back(r.y1);
        coords_y.push_back(r.y2);
    }
    if (events.empty()) return 0;

    std::sort(events.begin(), events.end());
    std::sort(coords_y.begin(), coords_y.end());
    coords_y.erase(std::unique(coords_y.begin(), coords_y.end()), coords_y.end());

    const int m = static_cast<int>(coords_y.size()) - 1; // Number of elementary y-intervals
    if (m <= 0) return 0;

    std::vector<int> cnt(4 * m, 0);
    std::vector<int64_t> len(4 * m, 0);

    auto pull = [&](int u, int l, int r) {
        if (cnt[u] > 0) {
            len[u] = coords_y[r + 1] - coords_y[l];
        } else if (l == r) {
            len[u] = 0;
        } else {
            len[u] = len[2 * u] + len[2 * u + 1];
        }
    };

    auto update = [&](auto& self, int u, int l, int r, int ql, int qr, int delta) -> void {
        if (ql <= l && r <= qr) {
            cnt[u] += delta;
            pull(u, l, r);
            return;
        }
        int mid = l + (r - l) / 2;
        if (ql <= mid) self(self, 2 * u, l, mid, ql, qr, delta);
        if (qr > mid)  self(self, 2 * u + 1, mid + 1, r, ql, qr, delta);
        pull(u, l, r);
    };

    int64_t total_area = 0;
    int64_t prev_x = events[0].x;

    for (const auto& ev : events) {
        total_area += len[1] * (ev.x - prev_x);
        prev_x = ev.x;

        int yl = static_cast<int>(std::lower_bound(coords_y.begin(), coords_y.end(), ev.y1) - coords_y.begin());
        int yr = static_cast<int>(std::lower_bound(coords_y.begin(), coords_y.end(), ev.y2) - coords_y.begin()) - 1;
        if (yl <= yr) {
            update(update, 1, 0, m - 1, yl, yr, ev.type);
        }
    }
    return total_area;
}

Production Pitfall — Point-Based vs Elementary-Interval Indexing in Segment Trees

In a standard point Segment Tree, leaf $i$ represents point $Y[i]$, so $[Y[i], Y[i+1]]$ leaves a gap between $Y[i]$ and $Y[i+1]$! In Klee's Measure Problem, leaf $i$ MUST represent the continuous elementary interval $[Y[i], Y[i+1]]$ of length coords_y[i + 1] - coords_y[i]. Consequently, a rectangle spanning $[y_1, y_2]$ updates leaf indices [idx(y1), idx(y2) - 1], not [idx(y1), idx(y2)]!

Check yourself Question 1 of 4
Concept Check Core Invariant Check recall

Why does the Coverage Segment Tree used in the Sweep-Line algorithm for the Area of Union of $N$ Rectangles (Klee's Measure Problem) **not** require lazy propagation (`push_down`)?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

In the Sweep-Line algorithm for the 2D Closest Pair of Points (maintaining active points within $x$-distance $< d$ in a `std::set` ordered by $y$), why does the inner loop scanning points in $[y_i - d, y_i + d]$ inspect at most **6 points** per step?

Key Takeaways

The Sweep-Line Paradigm replaces static 2D geometry with a 1D status structure advancing across sorted x-event points.
Shamos-Hoey and Bentley-Ottmann rely on the invariant that two segments must become immediately adjacent in the vertical sweep-line status before they can intersect.
Area of Union of N Rectangles (Klee's Measure Problem) runs in O(N log N) using a coverage-count Segment Tree over elementary intervals [Y[i], Y[i+1]] without needing lazy propagation.
Leaf i in a measure Segment Tree represents the continuous span [Y[i], Y[i+1]], so a vertical edge [y1, y2] updates leaf indices [idx(y1), idx(y2) - 1].
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue