112 lines
4.0 KiB
Python
112 lines
4.0 KiB
Python
"""Interval overlap detection via sweep-line.
|
|
|
|
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.
|
|
|
|
Complexity of `find_overlapping_pairs` is O(n log n + k), where n is the
|
|
number of intervals and k is the number of overlapping pairs it returns
|
|
(k can be Theta(n^2), so the sort bound alone does not describe it).
|
|
`has_any_overlap` answers the boolean question in O(n log n) and stops at
|
|
the first overlapping pair, without building the pair list.
|
|
"""
|
|
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:
|
|
"""True if any two half-open intervals overlap; stops at the first pair.
|
|
|
|
O(n log n) time, O(n) space: it never materialises the pair list, so a
|
|
fully-overlapping input costs one comparison rather than n*(n-1)/2 pairs.
|
|
"""
|
|
iv = list(intervals)
|
|
for s, e in iv:
|
|
if e < s:
|
|
raise ValueError(f"end < start: {(s, e)}")
|
|
# Sorted by start, every earlier start is <= the current one, so an
|
|
# overlap exists iff some earlier non-empty interval ends after this
|
|
# start. Tracking the running maximum end is enough.
|
|
max_end = None
|
|
for s, e in sorted(iv):
|
|
if s >= e: # zero-length interval: empty set, never overlaps
|
|
continue
|
|
if max_end is not None and s < max_end:
|
|
return True
|
|
if max_end is None or e > max_end:
|
|
max_end = e
|
|
return False
|
|
|
|
|
|
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")
|