From b14d2e42cba6f540255757fa6817c2cff84a1ffa Mon Sep 17 00:00:00 2001 From: ledger Date: Tue, 27 Oct 2026 00:38:24 +0000 Subject: [PATCH] beam: cells for interval_overlap.py -- CHECK 202 -> 214, rc 0; docstring names has_any_overlap, early exit added (Q84) --- wizard-work/beam/interval_overlap.py | 30 +++++- wizard-work/beam/test_interval_overlap.py | 124 ++++++++++++++++++++++ 2 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 wizard-work/beam/test_interval_overlap.py diff --git a/wizard-work/beam/interval_overlap.py b/wizard-work/beam/interval_overlap.py index 01d5f3a..047351c 100644 --- a/wizard-work/beam/interval_overlap.py +++ b/wizard-work/beam/interval_overlap.py @@ -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 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. +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 @@ -46,7 +50,27 @@ def find_overlapping_pairs(intervals: Iterable[Interval]) -> List[Tuple[int, int 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]]: diff --git a/wizard-work/beam/test_interval_overlap.py b/wizard-work/beam/test_interval_overlap.py new file mode 100644 index 0000000..c91d7ce --- /dev/null +++ b/wizard-work/beam/test_interval_overlap.py @@ -0,0 +1,124 @@ +"""Cells for interval_overlap.py -- collected by `unittest discover` in beam/. + +Covers half-open adjacency, zero-length intervals, the end 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)