beam: interval overlap sweep-line with half-open + zero-length handling

This commit is contained in:
beam committed 2026-10-26 22:48:49 +00:00
1 parent 0eb3f98c74
commit 517362fdaa
1 file changed
+87
+87
View File
@@ -0,0 +1,87 @@
"""Interval overlap detection via sweep-line (O(n log n)).
Given a list of half-open intervals [start, end), returns all unordered
pairs (i, j) with i < j whose intervals intersect (share at least one
point). Half-open semantics: [1,2) and [2,3) do NOT overlap.
Also exposes `count_overlaps` for the cheaper "is any overlap" style query.
"""
from __future__ import annotations
from typing import Iterable, List, Tuple
Interval = Tuple[int, int]
def find_overlapping_pairs(intervals: Iterable[Interval]) -> List[Tuple[int, int]]:
"""Return sorted list of (i, j) index pairs (i<j) whose intervals overlap."""
iv = list(intervals)
n = len(iv)
# Validate
for s, e in iv:
if e < s:
raise ValueError(f"end < start: {(s, e)}")
# Sweep events: (point, kind, index). kind 0 = start, 1 = end so that
# an interval ending at x is removed before one starting at x (half-open).
events: List[Tuple[int, int, int]] = []
for idx, (s, e) in enumerate(iv):
events.append((s, 1, idx)) # start (kind 1) processed after ends at same point
events.append((e, 0, idx)) # end (kind 0) first for half-open semantics
events.sort(key=lambda x: (x[0], x[1]))
active: set = set()
pairs: set = set()
for point, kind, idx in events:
if kind == 1: # start
s, e = iv[idx]
if s >= e: # zero-length interval: empty set, never overlaps
continue
for other in active:
a, b = (idx, other) if idx < other else (other, idx)
pairs.add((a, b))
active.add(idx)
else: # end: remove
active.discard(idx)
return sorted(pairs)
def has_any_overlap(intervals: Iterable[Interval]) -> bool:
return bool(find_overlapping_pairs(intervals))
def brute_force_pairs(intervals: Iterable[Interval]) -> List[Tuple[int, int]]:
iv = list(intervals)
n = len(iv)
out = []
for i in range(n):
for j in range(i + 1, n):
s1, e1 = iv[i]
s2, e2 = iv[j]
# half-open overlap: max(starts) < min(ends)
if max(s1, s2) < min(e1, e2):
out.append((i, j))
return out
if __name__ == "__main__":
import random
random.seed(42)
for trial in range(2000):
n = random.randint(0, 12)
ivs = []
for _ in range(n):
a = random.randint(-5, 5)
b = a + random.randint(0, 6)
ivs.append((a, b))
got = find_overlapping_pairs(ivs)
exp = brute_force_pairs(ivs)
if got != exp:
print("MISMATCH", ivs, got, exp)
raise SystemExit(1)
print("all 2000 random trials passed")
# edge cases
assert find_overlapping_pairs([(1, 2), (2, 3)]) == []
assert find_overlapping_pairs([(1, 3), (2, 4)]) == [(0, 1)]
assert find_overlapping_pairs([]) == []
assert find_overlapping_pairs([(5, 5)]) == [] # zero-length never overlaps
print("edge cases passed")