Sweep Line

Move an imaginary line across the plane and maintain a data structure describing what it currently touches. The continuous problem becomes a discrete sequence of events.

The skeleton

// 1. build events, sorted by the sweep coordinate
vector<Event> ev;
sort(ev.begin(), ev.end());
 
// 2. a status structure of currently-active objects
set<Segment, SweepCmp> status;
 
// 3. process
for (auto& e : ev) {
    if (e.isStart)  status.insert(e.obj);
    else            status.erase(e.obj);
    // answer whatever the event asks
}

Three design decisions: what is an event, what does the status structure hold, and what invariant does it maintain.

The classic applications

ProblemEventsStatusTime
Any two segments intersect?endpointssegments by (Shamos-Hoey)
Report all intersectionsendpoints + intersectionssegments by (Bentley-Ottmann)
Area of a union of rectanglesvertical edgessegment tree with counts
Perimeter of a union of rectanglessamesame, tracking transitions
Count rectangle overlapssamesegment tree with min-count
Closest pairpoints by set of points by — 16-closest-pair
Maximum points in a windowwindow edgescount in a BIT
Skyline / building silhouettebuilding edgesmultiset of heights
Point in polygon, many queriespolygon edges + queriesactive edges
Voronoisites + circle eventsthe beach line (Fortune) — 20-delaunay-and-voronoi
Rectangle stabbing queriesedges + queriesBIT over

Area of a union of rectangles

The template everyone should know: sweep , keep a segment tree over compressed that tracks how much of the -axis is currently covered.

struct CoverTree {
    vector<ll> ys;
    vector<int> cnt;
    vector<ll> len;
    explicit CoverTree(vector<ll> coords) : ys(coords), cnt(4 * coords.size(), 0), len(4 * coords.size(), 0) {}
    void update(int v, int l, int r, int ql, int qr, int delta) {   // node covers [ys[l], ys[r]]
        if (qr <= l || r <= ql) return;
        if (ql <= l && r <= qr) {
            cnt[v] += delta;
        } else {
            int m = (l + r) / 2;
            update(2 * v, l, m, ql, qr, delta);
            update(2 * v + 1, m, r, ql, qr, delta);
        }
        if (cnt[v]) len[v] = ys[r] - ys[l];
        else len[v] = r - l == 1 ? 0 : len[2 * v] + len[2 * v + 1];
    }
    void add(ll y1, ll y2, int delta) {
        int l = lower_bound(ys.begin(), ys.end(), y1) - ys.begin(), r = lower_bound(ys.begin(), ys.end(), y2) - ys.begin();
        update(1, 0, ys.size() - 1, l, r, delta);
    }
    ll covered() const { return len[1]; }
};
 
ll rectangleUnionArea(const vector<array<ll, 4>>& rects) {   // {x1, y1, x2, y2}, x1 < x2, y1 < y2
    if (rects.empty()) return 0;
    vector<ll> ys;
    vector<tuple<ll, int, ll, ll>> events;   // x, +1 / -1, y1, y2
    for (auto [x1, y1, x2, y2] : rects) {
        ys.push_back(y1); ys.push_back(y2);
        events.push_back({x1, 1, y1, y2}); events.push_back({x2, -1, y1, y2});
    }
    sort(ys.begin(), ys.end());
    ys.erase(unique(ys.begin(), ys.end()), ys.end());
    sort(events.begin(), events.end());
    CoverTree tree(ys);
    ll area = 0, lastX = get<0>(events[0]);
    for (auto [x, type, y1, y2] : events) {
        area += tree.covered() * (x - lastX);
        lastX = x;
        tree.add(y1, y2, type);
    }
    return area;
}
 
ll rectangleUnionPerimeter(const vector<array<ll, 4>>& rects) {
    auto sweep = [&](bool swapAxes) {
        vector<ll> ys;
        vector<tuple<ll, int, ll, ll>> events;
        for (auto r4 : rects) {
            ll x1 = r4[0], y1 = r4[1], x2 = r4[2], y2 = r4[3];
            if (swapAxes) { swap(x1, y1); swap(x2, y2); }
            ys.push_back(y1); ys.push_back(y2);
            events.push_back({x1, -1, y1, y2}); events.push_back({x2, 1, y1, y2});   // adds (-1) sort first
        }
        sort(ys.begin(), ys.end());
        ys.erase(unique(ys.begin(), ys.end()), ys.end());
        sort(events.begin(), events.end());
        CoverTree tree(ys);
        ll total = 0;
        for (auto [x, type, y1, y2] : events) {
            ll before = tree.covered();
            tree.add(y1, y2, -type);
            total += llabs(tree.covered() - before);
        }
        return total;
    };
    if (rects.empty()) return 0;
    return sweep(false) + sweep(true);
}

Complexity: . The segment tree stores, per node, a count of covering intervals and the covered length; crucially it never needs lazy propagation, because updates always come in matching pairs, so a node’s count never goes negative and the covered length is recomputable from the count and children. Coordinate-compress the values first.

Tested: rectangleUnionArea against the circle/polygon-union Monte-Carlo pattern used elsewhere (random sample points, fraction inside any rectangle, scaled by bounding box); rectangleUnionPerimeter against a direct trace of the union region’s boundary length on small test cases built from known configurations (disjoint, touching, nested, overlapping rectangles) where the true perimeter can be computed by hand.

Choosing the status structure

NeedStructure
Ordered active objects with neighboursstd::set
Count/sum over a coordinate rangeBIT or segment tree
Covered length under updatessegment tree with counts (above)
Minimum/maximum active valuemultiset or a heap with lazy deletion
-th active valuePBDS or a BIT

Radial and angular sweeps

The sweep need not be a line. Rotating a ray around a point (sorting by angle) solves: visibility polygons, “how many points lie in some half-plane through this point”, maximum points inside an angular sector, counting triangles containing the origin.

Sort by angle with the exact cross-product comparator, not atan2.

Practical notes

Event ordering at equal coordinates

When several events share a sweep coordinate, their relative order matters. For rectangle unions, process all additions before all removals at the same (or you will miss zero-width overlaps). Encode the tiebreak explicitly in the comparator rather than relying on the sort being stable.

  • Coordinate-compress early; it makes the status structure a small array.
  • Use exact integer comparators; the whole point of a sweep is that the decisions are combinatorial.
  • Think about whether the sweep should be over , over angle, or over time (see offline dynamic connectivity, a sweep over the query timeline).

Shamos-Hoey

Decide whether any two of line segments intersect, in — without finding all the intersections. The original sweep-line algorithm (1976).

Bentley-Ottmann reports all intersections in , which is when everything crosses everything. But a yes/no answer only needs the first crossing, which always involves two segments adjacent in the sweep-line status at some moment.

Algorithm

  1. Create events: the left and right endpoint of each segment. Sort by (ties: left endpoints first, then by ).
  2. Maintain a status structure — a balanced BST of the segments currently crossing the sweep line, ordered by their at the sweep position.
  3. Process events left to right:
    • Left endpoint of : insert . Test against its immediate predecessor and successor. If either intersects, report yes and stop.
    • Right endpoint of : before erasing , test its predecessor against its successor (they become adjacent). If they intersect, report yes and stop.
  4. If the sweep finishes, report no.
bool anyIntersection(vector<BOSeg> segs) {       // BOSeg, StatusCmp, sweepX_: see Bentley-Ottmann below
    vector<pair<double, int>> ev;                 // x, segment index (sign encodes left/right in practice)
    for (int i = 0; i < (int)segs.size(); i++) { ev.push_back({segs[i].a.x, i}); ev.push_back({segs[i].b.x, ~i}); }
    sort(ev.begin(), ev.end());
    set<BOSeg, StatusCmp> status;
    map<int, set<BOSeg, StatusCmp>::iterator> where;
    for (auto [x, code] : ev) {
        sweepX_ = x;
        if (code >= 0) {
            auto it = status.insert(segs[code]).first;
            where[code] = it;
            auto nx = next(it), pv = (it == status.begin() ? status.end() : prev(it));
            PD tmp;
            if (nx != status.end() && segIntersect(*it, *nx, tmp)) return true;
            if (pv != status.end() && segIntersect(*pv, *it, tmp)) return true;
        } else {
            int id = ~code;
            auto it = where[id];
            auto nx = next(it), pv = (it == status.begin() ? status.end() : prev(it));
            PD tmp;
            if (nx != status.end() && pv != status.end() && segIntersect(*pv, *nx, tmp)) return true;
            status.erase(it);
        }
    }
    return false;
}

Complexity: — events, each BST work and intersection tests.

Shamos-Hoey vs Bentley-Ottmann

Shamos-HoeyBentley-Ottmann
Answersdoes any intersection existall intersections
Time
Eventsendpoints onlyendpoints and intersection points
Status changesinsert/eraseinsert/erase and swap

Variants / Use Cases

  • Simple polygon testing — is a polygon self-intersecting? Feed its edges (excluding shared endpoints) to Shamos-Hoey.
  • Planarity of a drawing — does a given straight-line drawing have crossings?
  • Validating geometric input — GIS and CAD systems run this constantly.

Bentley-Ottmann

Find all pairwise intersection points among line segments in , the number of intersections.

Algorithm

  1. Sweep a vertical line left to right. Maintain an event queue (ordered by , then ) of segment endpoints and detected intersection points.
  2. Maintain a status structure ordered by each segment’s current at the sweep position.
  3. Left endpoint: insert; check the new neighbours for intersections, scheduling any found as future events.
  4. Right endpoint: remove; check the now-adjacent former neighbours against each other.
  5. Intersection event: report the point; the two segments swap -order here, so remove and reinsert both (their relative order has flipped), then check each against its new far-side neighbour.
struct BOSeg { PD a, b; int id; };   // caller guarantees a.x < b.x; no vertical segments (see the scope note below)
 
double sweepX_;                       // current sweep position, read by the status comparator
double yAtSweep(const BOSeg& s, double x) {
    double t = (x - s.a.x) / (s.b.x - s.a.x);
    return s.a.y + t * (s.b.y - s.a.y);
}
struct StatusCmp {
    bool operator()(const BOSeg& s1, const BOSeg& s2) const {
        double y1 = yAtSweep(s1, sweepX_), y2 = yAtSweep(s2, sweepX_);
        if (fabs(y1 - y2) > 1e-9) return y1 < y2;
        double sl1 = (s1.b.y - s1.a.y) / (s1.b.x - s1.a.x), sl2 = (s2.b.y - s2.a.y) / (s2.b.x - s2.a.x);
        if (fabs(sl1 - sl2) > 1e-9) return sl1 < sl2;
        return s1.id < s2.id;
    }
};
struct BOEvent {
    double x, y; int type; int id1, id2;       // type: 0 left, 1 right, 2 intersection
    bool operator<(const BOEvent& o) const { return x > o.x || (x == o.x && y > o.y); }  // reversed: min-heap
};
 
bool segIntersect(const BOSeg& s1, const BOSeg& s2, PD& out) {
    PD d1 = s1.b - s1.a, d2 = s2.b - s2.a;
    double den = cross(d1, d2);
    if (fabs(den) < 1e-12) return false;                 // parallel (collinear overlap: out of scope, see below)
    double t = cross(s2.a - s1.a, d2) / den, u = cross(s2.a - s1.a, d1) / den;
    if (t < -1e-9 || t > 1 + 1e-9 || u < -1e-9 || u > 1 + 1e-9) return false;
    out = s1.a + d1 * t;
    return true;
}
 
vector<PD> bentleyOttmann(vector<BOSeg> segs) {
    priority_queue<BOEvent> pq;
    map<int, BOSeg> byId;
    for (auto& s : segs) {
        byId[s.id] = s;
        pq.push({s.a.x, s.a.y, 0, s.id, -1});
        pq.push({s.b.x, s.b.y, 1, s.id, -1});
    }
    set<BOSeg, StatusCmp> status;
    map<int, set<BOSeg, StatusCmp>::iterator> where;
    set<pair<int, int>> scheduled;
    vector<PD> result;
 
    auto schedule = [&](int id1, int id2) {
        if (id1 < 0 || id2 < 0) return;
        auto key = minmax(id1, id2);
        if (scheduled.count(key)) return;
        PD pt;
        if (segIntersect(byId[id1], byId[id2], pt) && pt.x >= sweepX_ - 1e-9) {
            pq.push({pt.x, pt.y, 2, id1, id2});
            scheduled.insert(key);
        }
    };
 
    while (!pq.empty()) {
        BOEvent e = pq.top(); pq.pop();
        sweepX_ = e.x;
        if (e.type == 0) {
            auto it = status.insert(byId[e.id1]).first;
            where[e.id1] = it;
            auto nx = next(it), pv = (it == status.begin() ? status.end() : prev(it));
            if (nx != status.end()) schedule(e.id1, nx->id);
            if (pv != status.end()) schedule(e.id1, pv->id);
        } else if (e.type == 1) {
            auto it = where[e.id1];
            auto nx = next(it), pv = (it == status.begin() ? status.end() : prev(it));
            if (nx != status.end() && pv != status.end()) schedule(pv->id, nx->id);
            status.erase(it);
            where.erase(e.id1);
        } else {
            result.push_back({e.x, e.y});                  // report the intersection
            status.erase(where[e.id1]); status.erase(where[e.id2]);
            sweepX_ = e.x + 1e-7;                            // just past the crossing: order has now flipped
            bool firstIsLower = StatusCmp()(byId[e.id1], byId[e.id2]);
            int loId = firstIsLower ? e.id1 : e.id2, hiId = firstIsLower ? e.id2 : e.id1;
            auto insLo = status.insert(byId[loId]).first;
            auto insHi = status.insert(byId[hiId]).first;
            where[loId] = insLo; where[hiId] = insHi;
            auto beforeLo = (insLo == status.begin() ? status.end() : prev(insLo));
            auto afterHi = next(insHi);
            if (beforeLo != status.end()) schedule(beforeLo->id, loId);
            if (afterHi != status.end()) schedule(hiId, afterHi->id);
        }
    }
    return result;
}

Complexity: : endpoint events plus intersection events, each .

Scope of this implementation

This version handles the generic-position case cleanly: no vertical segments (yAtSweep divides by ), no two segments overlapping along a shared sub-segment (segIntersect returns “no intersection” for parallel lines, including collinear-overlapping ones), and no three or more segments crossing at exactly the same point (each crossing is scheduled and resolved as a pairwise event; a triple point works out to the same reported point from multiple pairs in practice, but the status-structure bookkeeping around it is not guaranteed order-independent). A production implementation handles all three with extra casework — rotate the input slightly to remove verticals, treat collinear overlaps as their own event type, and merge simultaneous intersection events before processing — but for contest inputs in general position, this is the real algorithm, not a sketch.

Tested against brute-force all-pairs intersection on random segment sets (up to 25 segments, hundreds of trials), excluding by construction the three degeneracies noted above (verticals, collinear overlaps, triple points) to match the implementation’s documented scope; every brute-force intersection point is matched by a reported one and vice versa.

Variants / Use Cases

  • Map overlay / GIS polygon intersection — core subroutine for combining vector map layers.
  • CAD system geometry engines — detecting overlaps and crossings in drawings.
  • Axis-aligned rectangle intersection — a simplified variant for orthogonal shapes.
  • Arrangement construction — generalises to arrangements of curves, not just straight segments.