beam: interval overlap sweep-line with half-open + zero-length handling
This commit is contained in:
1 parent
0eb3f98c74
commit
517362fdaa
1 file changed
+87
@@ -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")
|
||||
Reference in new issue
Block a user