17.10 Interval Geometry

You need to solve geometric problems that can be reduced to one-dimensional intervals.

17.10 Interval Geometry

Problem

You need to solve geometric problems that can be reduced to one-dimensional intervals.

Many two-dimensional problems contain an interval subproblem. Rectangle union, line sweep algorithms, calendar conflicts, range coverage, collision detection, and spatial indexing all require operations such as:

insert an interval
delete an interval
merge overlapping intervals
measure total covered length
find whether a point is covered
find maximum overlap

Getting interval geometry right is important because sweep-line algorithms frequently delegate their active-state logic to interval routines.

Solution

Represent each interval with a start and end coordinate.

For most algorithms, normalize the interval so that:

start <= end

A simple implementation:

from dataclasses import dataclass

@dataclass(frozen=True)
class Interval:
    start: float
    end: float

    def __post_init__(self):
        if self.start > self.end:
            raise ValueError("interval start must be <= end")

For integer coordinates, the same structure works with int.

The main design decision is whether intervals are closed, open, or half-open.

For algorithmic work, half-open intervals are often the cleanest:

[start, end)

This means start is included and end is excluded.

Half-open intervals avoid double-counting boundaries when adjacent intervals touch.

[1, 3) and [3, 5)

cover length:

4

not:

5

and not an overlapping point at 3.

Merging Intervals

Given intervals:

[1, 4)
[2, 6)
[8, 10)
[9, 12)

the merged result is:

[1, 6)
[8, 12)

Implementation:

def merge_intervals(intervals):
    if not intervals:
        return []

    intervals = sorted(intervals, key=lambda interval: interval.start)

    merged = [intervals[0]]

    for current in intervals[1:]:
        previous = merged[-1]

        if current.start <= previous.end:
            merged[-1] = Interval(
                previous.start,
                max(previous.end, current.end)
            )
        else:
            merged.append(current)

    return merged

For half-open intervals, decide whether touching intervals should merge.

If [1, 3) and [3, 5) should remain separate, use:

if current.start < previous.end:
    ...

If they should merge into [1, 5), use:

if current.start <= previous.end:
    ...

This choice depends on the problem.

Total Covered Length

After merging intervals, total coverage is straightforward:

def covered_length(intervals):
    merged = merge_intervals(intervals)

    total = 0

    for interval in merged:
        total += interval.end - interval.start

    return total

Example:

[1, 4)
[2, 6)
[8, 10)
[9, 12)

Merged:

[1, 6)
[8, 12)

Covered length:

(6 - 1) + (12 - 8) = 9

This routine appears directly inside rectangle-union and sweep-line area algorithms.

Point Coverage

To test whether a point lies inside any interval, first merge the intervals, then binary search.

from bisect import bisect_right

def contains_point(intervals, x):
    merged = merge_intervals(intervals)
    starts = [interval.start for interval in merged]

    index = bisect_right(starts, x) - 1

    if index < 0:
        return False

    interval = merged[index]

    return interval.start <= x < interval.end

For many queries, merge once and reuse the result.

class IntervalSet:
    def __init__(self, intervals):
        self.intervals = merge_intervals(intervals)
        self.starts = [
            interval.start
            for interval in self.intervals
        ]

    def contains(self, x):
        index = bisect_right(self.starts, x) - 1

        if index < 0:
            return False

        interval = self.intervals[index]

        return interval.start <= x < interval.end

This gives:

O(log n)

query time after preprocessing.

Maximum Overlap

Given intervals, find the largest number of intervals covering the same point.

Use events:

start event: +1
end event:   -1

Implementation:

def maximum_overlap(intervals):
    events = []

    for interval in intervals:
        events.append((interval.start, 1))
        events.append((interval.end, -1))

    events.sort(key=lambda event: (event[0], event[1]))

    active = 0
    best = 0

    for _, delta in events:
        active += delta
        best = max(best, active)

    return best

With half-open intervals, end events should be processed before start events at the same coordinate.

That is why sorting by:

(event[0], event[1])

works: -1 comes before +1.

Example:

[1, 3)
[3, 5)

The first interval ends exactly when the second starts. They do not overlap.

Finding Overlap Between Two Interval Sets

Given two sorted, non-overlapping interval lists, find all intersections.

def intersect_interval_sets(a, b):
    i = 0
    j = 0
    result = []

    while i < len(a) and j < len(b):
        start = max(a[i].start, b[j].start)
        end = min(a[i].end, b[j].end)

        if start < end:
            result.append(Interval(start, end))

        if a[i].end < b[j].end:
            i += 1
        else:
            j += 1

    return result

This is a two-pointer scan.

Time complexity:

O(n + m)

where n and m are the sizes of the two interval sets.

Rectangle Projection

A rectangle can be projected onto the x-axis and y-axis.

For a rectangle:

left, bottom, right, top

the projections are:

x interval: [left, right)
y interval: [bottom, top)

Two axis-aligned rectangles overlap if both projections overlap.

@dataclass(frozen=True)
class Rectangle:
    left: float
    bottom: float
    right: float
    top: float

def intervals_overlap(a, b):
    return a.start < b.end and b.start < a.end

def rectangles_overlap(a, b):
    ax = Interval(a.left, a.right)
    ay = Interval(a.bottom, a.top)

    bx = Interval(b.left, b.right)
    by = Interval(b.bottom, b.top)

    return intervals_overlap(ax, bx) and intervals_overlap(ay, by)

This projection idea is one of the simplest examples of interval geometry inside two-dimensional geometry.

Dynamic Interval Coverage

Some sweep-line algorithms require intervals to be inserted and deleted while repeatedly asking for total covered length.

A simple list-based implementation is often enough for small inputs:

class DynamicIntervalSet:
    def __init__(self):
        self.intervals = []

    def add(self, interval):
        self.intervals.append(interval)

    def remove(self, interval):
        self.intervals.remove(interval)

    def covered_length(self):
        return covered_length(self.intervals)

This is easy to reason about, but inefficient.

For large inputs, use a segment tree or balanced interval tree. Rectangle-union algorithms commonly use coordinate compression plus a segment tree to maintain covered y-length while sweeping across x-events.

Coordinate Compression

When intervals use many large coordinates but only endpoints matter, compress coordinates.

Example endpoints:

1000000000
1000000050
1000000100

can become:

0
1
2

while preserving order.

def compress_coordinates(values):
    sorted_values = sorted(set(values))

    index = {
        value: i
        for i, value in enumerate(sorted_values)
    }

    return index, sorted_values

Coordinate compression is especially useful when building segment trees over interval endpoints.

Common Pitfalls

Mixing Closed and Half-Open Semantics

Decide early whether intervals mean:

[start, end]

or:

[start, end)

Most geometry and sweep-line code is easier with half-open intervals.

Incorrect Tie-Breaking

For half-open intervals, process end events before start events at the same coordinate.

For closed intervals, the opposite may be required.

Merging When You Should Not

Touching intervals may or may not count as connected coverage. Define this explicitly.

Recomputing Merged Intervals Repeatedly

If many point queries are needed, merge once and query with binary search.

Ignoring Degenerate Intervals

Intervals with:

start = end

have zero length under half-open semantics. Decide whether to keep or discard them.

Complexity Analysis

Merging intervals:

O(n log n)

because of sorting.

Computing covered length after merging:

O(n)

Point query after preprocessing:

O(log n)

Two-set intersection:

O(n + m)

Dynamic coverage depends on the chosen data structure. A simple list is easy but slow. A segment tree supports efficient updates and coverage queries after coordinate compression.

Discussion

Interval geometry is the one-dimensional substrate behind many higher-dimensional algorithms. Sweep-line methods depend on it. Rectangle union area depends on it. Collision detection often reduces to interval overlap on projected axes. Scheduling systems, resource allocators, and temporal databases use the same patterns under different names.

The central discipline is semantic clarity. Once you decide whether intervals are closed, open, or half-open, every comparison, merge condition, and event-ordering rule follows from that choice.