Line Sweep Algorithm: Computational Geometry

Line sweep is one of those algorithmic paradigms that, once internalized, makes you see geometry problems differently. The core idea is deceptively simple: instead of reasoning about objects...

Key Insights

  • Line sweep transforms complex 2D geometric problems into manageable 1D problems by processing events in sorted order, reducing both conceptual and computational complexity.
  • The algorithm requires two core data structures working in tandem: an event queue (priority queue) to determine what happens next, and a status structure (balanced BST) to track what’s currently active.
  • Most competitive programming geometry problems—from rectangle unions to closest pairs—become straightforward once you recognize them as line sweep candidates.

Introduction to Line Sweep

Line sweep is one of those algorithmic paradigms that, once internalized, makes you see geometry problems differently. The core idea is deceptively simple: instead of reasoning about objects scattered across a 2D plane, you sweep an imaginary vertical line from left to right, processing events as you encounter them.

This mental shift is powerful. A static 2D problem becomes a dynamic 1D problem. At any moment, your sweep line intersects only a subset of objects, and you maintain this subset in a data structure that supports efficient queries and updates.

Consider finding all intersections among line segments. The naive approach checks every pair—O(n²) comparisons. But segments can only intersect if they’re “active” at the same x-coordinate. Line sweep exploits this by only comparing segments that are neighbors in the current sweep line status.

Core Components and Data Structures

Every line sweep algorithm has two essential components:

Event Queue: A priority queue containing all x-coordinates where something interesting happens. Events are processed left-to-right (smallest x first). Typical events include object starts, object ends, and intersection points.

Sweep Line Status: A balanced binary search tree (or similar ordered structure) maintaining objects currently intersected by the sweep line, ordered by their y-coordinate at the current x position.

The interaction between these structures is the algorithm’s heartbeat: pop an event, update the status, possibly discover new events to add to the queue.

from dataclasses import dataclass
from enum import Enum
from heapq import heappush, heappop
from sortedcontainers import SortedList

class EventType(Enum):
    START = 0      # Object begins
    END = 2        # Object ends
    INTERSECT = 1  # Special event (e.g., intersection)

@dataclass(order=True)
class Event:
    x: float
    event_type: EventType
    data: object  # Segment reference, point, etc.

class LineSweep:
    def __init__(self):
        self.events = []  # Min-heap by (x, event_type)
        self.status = SortedList()  # Active objects ordered by y
    
    def add_event(self, x: float, event_type: EventType, data):
        heappush(self.events, (x, event_type.value, data))
    
    def process(self):
        results = []
        while self.events:
            x, event_type_val, data = heappop(self.events)
            event_type = EventType(event_type_val)
            
            if event_type == EventType.START:
                self.handle_start(x, data, results)
            elif event_type == EventType.END:
                self.handle_end(x, data, results)
            else:
                self.handle_special(x, data, results)
        
        return results

The SortedList from the sortedcontainers library provides O(log n) insertions and deletions with O(log n) neighbor queries—exactly what we need for the status structure.

Classic Application: Line Segment Intersection

The Bentley-Ottmann algorithm is the canonical line sweep application. Given n line segments, it finds all k intersections in O((n + k) log n) time, compared to O(n²) for brute force.

The key insight: two segments can only intersect if they become adjacent in the sweep line status at some point. When we insert or remove a segment, we check for intersections with its new neighbors. When two segments swap their vertical order (at an intersection), we check their new neighbors.

from dataclasses import dataclass
from sortedcontainers import SortedList
import heapq

@dataclass
class Segment:
    x1: float
    y1: float
    x2: float
    y2: float
    id: int
    
    def y_at(self, x: float) -> float:
        """Get y-coordinate at given x."""
        if self.x1 == self.x2:
            return self.y1
        t = (x - self.x1) / (self.x2 - self.x1)
        return self.y1 + t * (self.y2 - self.y1)

def ccw(ax, ay, bx, by, cx, cy):
    """Counter-clockwise test."""
    return (cy - ay) * (bx - ax) > (by - ay) * (cx - ax)

def segments_intersect(s1: Segment, s2: Segment) -> bool:
    """Check if two segments intersect."""
    a, b = (s1.x1, s1.y1), (s1.x2, s1.y2)
    c, d = (s2.x1, s2.y1), (s2.x2, s2.y2)
    
    return (ccw(*a, *c, *d) != ccw(*b, *c, *d) and 
            ccw(*a, *b, *c) != ccw(*a, *b, *d))

def find_intersections(segments: list[Segment]) -> list[tuple[int, int]]:
    """Find all intersecting segment pairs using line sweep."""
    START, END = 0, 1
    events = []
    
    for seg in segments:
        left_x = min(seg.x1, seg.x2)
        right_x = max(seg.x1, seg.x2)
        heapq.heappush(events, (left_x, START, seg))
        heapq.heappush(events, (right_x, END, seg))
    
    current_x = float('-inf')
    # Custom key function for ordering by y at current x
    active = SortedList(key=lambda s: (s.y_at(current_x), s.id))
    intersections = []
    checked = set()
    
    def check_neighbors(seg):
        idx = active.index(seg)
        neighbors = []
        if idx > 0:
            neighbors.append(active[idx - 1])
        if idx < len(active) - 1:
            neighbors.append(active[idx + 1])
        
        for neighbor in neighbors:
            pair = tuple(sorted([seg.id, neighbor.id]))
            if pair not in checked:
                checked.add(pair)
                if segments_intersect(seg, neighbor):
                    intersections.append(pair)
    
    while events:
        current_x, event_type, seg = heapq.heappop(events)
        
        if event_type == START:
            active.add(seg)
            check_neighbors(seg)
        else:
            # Check neighbors before removal
            idx = active.index(seg)
            if 0 < idx < len(active) - 1:
                left, right = active[idx - 1], active[idx + 1]
                pair = tuple(sorted([left.id, right.id]))
                if pair not in checked:
                    checked.add(pair)
                    if segments_intersect(left, right):
                        intersections.append(pair)
            active.remove(seg)
    
    return intersections

This simplified version handles the core logic. A production implementation would also handle intersection events (where segments swap order) and degenerate cases like overlapping segments.

Interval Problems: Overlapping Rectangles

Rectangle problems are line sweep’s bread and butter. To compute the total area covered by overlapping rectangles, sweep vertically and maintain the currently covered horizontal intervals.

def rectangle_union_area(rectangles: list[tuple[int, int, int, int]]) -> int:
    """
    Calculate total area covered by rectangles.
    Each rectangle is (x1, y1, x2, y2) where (x1,y1) is bottom-left.
    """
    OPEN, CLOSE = 0, 1
    events = []
    
    for x1, y1, x2, y2 in rectangles:
        events.append((y1, OPEN, x1, x2))
        events.append((y2, CLOSE, x1, x2))
    
    events.sort()
    
    def calculate_covered_length(intervals: list[tuple[int, int]]) -> int:
        """Calculate total length covered by intervals."""
        if not intervals:
            return 0
        
        intervals.sort()
        total = 0
        current_start, current_end = intervals[0]
        
        for start, end in intervals[1:]:
            if start <= current_end:
                current_end = max(current_end, end)
            else:
                total += current_end - current_start
                current_start, current_end = start, end
        
        total += current_end - current_start
        return total
    
    active_intervals = []
    prev_y = events[0][0]
    total_area = 0
    
    for y, event_type, x1, x2 in events:
        # Add area contribution from previous y to current y
        covered_length = calculate_covered_length(active_intervals)
        total_area += covered_length * (y - prev_y)
        
        if event_type == OPEN:
            active_intervals.append((x1, x2))
        else:
            active_intervals.remove((x1, x2))
        
        prev_y = y
    
    return total_area

For better performance with many rectangles, replace the interval list with a segment tree that tracks coverage counts at each x-coordinate.

Closest Pair of Points

The closest pair problem demonstrates line sweep’s elegance. Sort points by x-coordinate, maintain a window of points within distance d (current best) of the sweep line, and use a balanced BST ordered by y-coordinate for efficient neighbor queries.

from sortedcontainers import SortedList
import math

def closest_pair(points: list[tuple[float, float]]) -> float:
    """Find minimum distance between any two points."""
    if len(points) < 2:
        return float('inf')
    
    # Sort by x-coordinate
    sorted_points = sorted(enumerate(points), key=lambda p: p[1][0])
    
    # Active points ordered by y-coordinate
    active = SortedList(key=lambda p: (p[1][1], p[0]))
    
    min_dist = float('inf')
    left = 0
    
    for right in range(len(sorted_points)):
        idx_r, (rx, ry) = sorted_points[right]
        
        # Remove points too far left
        while left < right:
            idx_l, (lx, ly) = sorted_points[left]
            if rx - lx >= min_dist:
                active.remove((idx_l, (lx, ly)))
                left += 1
            else:
                break
        
        # Check points within y-range [ry - min_dist, ry + min_dist]
        # Find insertion point and check neighbors
        for idx, (x, y) in active:
            if abs(y - ry) >= min_dist:
                continue
            dist = math.sqrt((rx - x) ** 2 + (ry - y) ** 2)
            min_dist = min(min_dist, dist)
        
        active.add((idx_r, (rx, ry)))
    
    return min_dist

The key optimization: within the active set, we only need to check points within ±d in the y-direction. With a balanced BST, we can find these candidates in O(log n) time, and there are at most O(1) such points (a geometric packing argument).

Complexity Analysis and Optimization Tips

Time Complexity Patterns:

  • Event processing: O(n log n) for sorting/heap operations
  • Status updates: O(log n) per event with balanced BST
  • Total: O(n log n) for most problems, O((n + k) log n) when output-sensitive

Common Pitfalls:

  1. Floating-point precision: Use epsilon comparisons or integer arithmetic when possible. Symbolic computation libraries help for exact geometry.

  2. Degenerate cases: Multiple events at the same x-coordinate, vertical segments, overlapping endpoints. Process events in a consistent order (e.g., starts before ends).

  3. Status structure ordering: The comparison key must be consistent. For segments, order by y-coordinate at the current sweep line position, with tie-breaking by segment ID.

When to Use Line Sweep vs. Divide-and-Conquer:

  • Line sweep: when you need to process all events or when the problem has an inherently sequential nature
  • Divide-and-conquer: when subproblems are independent and merge is efficient (like closest pair)

Both achieve O(n log n) for closest pair, but line sweep often has simpler implementation.

Practice Problems and Extensions

Essential Practice:

  • The Skyline Problem (LeetCode 218): Classic sweep with height tracking
  • Meeting Rooms II (LeetCode 253): Minimum conference rooms needed
  • Rectangle Area II (LeetCode 850): Union area with coordinate compression
  • Perfect Rectangle (LeetCode 391): Verify exact coverage

Extensions:

  • 3D Sweep Planes: Extend to three dimensions for problems like 3D intersection detection
  • Kinetic Data Structures: Handle moving objects where the sweep line status changes continuously
  • Rotational Sweep: Sweep by angle instead of x-coordinate for visibility problems

Line sweep is a fundamental technique that appears repeatedly in computational geometry, competitive programming, and real-world applications like GIS systems and CAD software. Master it, and a whole class of problems becomes tractable.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.