beam: cells for interval_overlap.py -- CHECK 202 -> 214, rc 0; docstring names has_any_overlap, early exit added (Q84)

This commit is contained in:
ledger committed 2026-10-27 00:38:24 +00:00
1 parent 83769897c1
commit b14d2e42cb
2 files changed
+151 -3

No files matched your search

+27 -3
View File
@@ -1,10 +1,14 @@
"""Interval overlap detection via sweep-line (O(n log n)). """Interval overlap detection via sweep-line.
Given a list of half-open intervals [start, end), returns all unordered 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 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. point). Half-open semantics: [1,2) and [2,3) do NOT overlap.
Also exposes `count_overlaps` for the cheaper "is any overlap" style query. 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 __future__ import annotations
from typing import Iterable, List, Tuple from typing import Iterable, List, Tuple
@@ -46,7 +50,27 @@ def find_overlapping_pairs(intervals: Iterable[Interval]) -> List[Tuple[int, int
def has_any_overlap(intervals: Iterable[Interval]) -> bool: def has_any_overlap(intervals: Iterable[Interval]) -> bool:
return bool(find_overlapping_pairs(intervals)) """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]]: def brute_force_pairs(intervals: Iterable[Interval]) -> List[Tuple[int, int]]:
+124
View File
@@ -0,0 +1,124 @@
"""Cells for interval_overlap.py -- collected by `unittest discover` in beam/.
Covers half-open adjacency, zero-length intervals, the end<start refusal, an
exhaustive cross-check of the sweep against the brute-force definition, and
`has_any_overlap` in both directions -- including the structural claim that it
does not build the pair list to answer a boolean.
"""
import itertools
import random
import time
import unittest
from unittest import mock
import interval_overlap as mod
def _domain(low=-2, high=3):
return [(s, e) for s in range(low, high + 1) for e in range(s, high + 1)]
class IntervalOverlapTest(unittest.TestCase):
def test_half_open_adjacency_both_orders(self):
self.assertEqual(mod.find_overlapping_pairs([(1, 2), (2, 3)]), [])
self.assertEqual(mod.find_overlapping_pairs([(2, 3), (1, 2)]), [])
def test_zero_length_against_wide_interval(self):
self.assertEqual(mod.find_overlapping_pairs([(5, 5), (1, 10)]), [])
self.assertEqual(mod.find_overlapping_pairs([(1, 10), (5, 5)]), [])
def test_zero_length_touching_boundary(self):
self.assertEqual(mod.find_overlapping_pairs([(2, 2), (1, 2)]), [])
self.assertEqual(mod.find_overlapping_pairs([(1, 2), (2, 2)]), [])
def test_zero_length_pair_is_empty(self):
self.assertEqual(mod.find_overlapping_pairs([(3, 3), (3, 3)]), [])
self.assertEqual(mod.find_overlapping_pairs([(0, 0)]), [])
def test_nested_and_disjoint_pairs(self):
self.assertEqual(
mod.find_overlapping_pairs([(1, 10), (2, 3), (4, 5)]),
[(0, 1), (0, 2)],
)
self.assertEqual(
mod.find_overlapping_pairs([(1, 3), (2, 4)]),
[(0, 1)],
)
def test_valueerror_when_end_before_start(self):
with self.assertRaises(ValueError):
mod.find_overlapping_pairs([(3, 1)])
with self.assertRaises(ValueError):
mod.has_any_overlap([(0, 5), (9, 2)])
def test_exhaustive_multisets_against_brute_force(self):
intervals = _domain()
self.assertEqual(len(intervals), 21)
cases = 0
for size in range(4):
for case in itertools.combinations_with_replacement(intervals, size):
cases += 1
ivs = list(case)
self.assertEqual(
mod.find_overlapping_pairs(ivs),
mod.brute_force_pairs(ivs),
msg="case %r" % (ivs,),
)
# Pins the enumeration itself: 21 multisets of size 0-3 -> 2024 cases.
self.assertEqual(cases, 2024)
def test_randomised_against_brute_force(self):
rng = random.Random(42)
for _ in range(2000):
ivs = []
for _ in range(rng.randint(0, 12)):
a = rng.randint(-5, 5)
ivs.append((a, a + rng.randint(0, 6)))
self.assertEqual(
mod.find_overlapping_pairs(ivs),
mod.brute_force_pairs(ivs),
msg="case %r" % (ivs,),
)
def test_has_any_overlap_true_and_false(self):
self.assertTrue(mod.has_any_overlap([(1, 3), (2, 4)]))
self.assertTrue(mod.has_any_overlap([(1, 10), (2, 3)]))
self.assertFalse(mod.has_any_overlap([(1, 2), (2, 3)]))
self.assertFalse(mod.has_any_overlap([(5, 5), (1, 10)]))
self.assertFalse(mod.has_any_overlap([]))
self.assertFalse(mod.has_any_overlap([(1, 2)]))
def test_has_any_overlap_agrees_with_pairs(self):
rng = random.Random(7)
for _ in range(500):
ivs = []
for _ in range(rng.randint(0, 10)):
a = rng.randint(-4, 4)
ivs.append((a, a + rng.randint(0, 5)))
self.assertEqual(
mod.has_any_overlap(ivs),
bool(mod.find_overlapping_pairs(ivs)),
msg="case %r" % (ivs,),
)
def test_has_any_overlap_does_not_build_pair_list(self):
# The "cheaper" claim is structural: answering the boolean must not
# route through the pair-list function. If it does, this cell dies.
with mock.patch.object(
mod, "find_overlapping_pairs", side_effect=AssertionError("materialised pairs")
):
self.assertTrue(mod.has_any_overlap([(0, 1), (0, 1)]))
def test_has_any_overlap_large_overlapping_input_is_cheap(self):
# 2000 intervals all sharing [0, 1): one comparison answers it. The
# pair-list function takes tens of seconds on this input.
ivs = [(0, 1000)] * 2000
start = time.perf_counter()
self.assertTrue(mod.has_any_overlap(ivs))
elapsed = time.perf_counter() - start
self.assertLess(elapsed, 5.0, "took %.2fs" % elapsed)
if __name__ == "__main__":
unittest.main(verbosity=2)