# SPDX-License-Identifier: MIT # Part of https://algorithm.tamnd.com/interval-tree-search/. The authors are listed in prov/C1KCSN.jsonl. """Search of a static augmented interval tree, for every end point type. The tree is an implicit balanced binary search tree over the intervals sorted by left end: the node of the index range [lo, hi) is mid = (lo + hi) // 2, its left subtree is [lo, mid) and its right subtree is [mid + 1, hi). Each node stores the largest right end of its subtree. A query is given by two monotone tests: reach(r) -- true for every right end r at or above a threshold, start(l) -- true for every left end l at or below a threshold. An interval matches when reach(r) and start(l). The table QUERY_TESTS gives the tests for overlap and stabbing queries on closed, half-open and open intervals. """ from typing import Callable, Optional, Sequence, Tuple Interval = Tuple[float, float] KINDS = ("closed", "half-open", "open") def overlap_tests(a, b, kind="closed"): """Tests for the query interval of the same type as the stored ones.""" if kind == "closed": # [l, r] meets [a, b] iff a <= r and l <= b return (lambda r: r >= a), (lambda l: l <= b) if kind in ("half-open", "open"): # strict at both cross conditions return (lambda r: r > a), (lambda l: l < b) raise ValueError("kind must be closed, half-open or open") def stab_tests(p, kind="closed"): """Tests for the query point p.""" if kind == "closed": # l <= p <= r return (lambda r: r >= p), (lambda l: l <= p) if kind == "half-open": # l <= p < r return (lambda r: r > p), (lambda l: l <= p) if kind == "open": # l < p < r return (lambda r: r > p), (lambda l: l < p) raise ValueError("kind must be closed, half-open or open") class IntervalTreeSearch: """Static interval tree; queries return indices into the input list.""" def __init__(self, intervals: Sequence[Interval]): self.iv = list(intervals) self.order = sorted(range(len(self.iv)), key=lambda j: (self.iv[j][0], self.iv[j][1], j)) n = len(self.order) self.max = [0.0] * n # max right end in the subtree whose root is position m self._build(0, n) def _build(self, lo, hi): if lo >= hi: return None mid = (lo + hi) // 2 best = self.iv[self.order[mid]][1] for child in (self._build(lo, mid), self._build(mid + 1, hi)): if child is not None and child > best: best = child self.max[mid] = best return best # REPORT: all matching intervals, in the order of the tree. def report(self, reach: Callable, start: Callable) -> list[int]: out: list[int] = [] self._report(0, len(self.order), reach, start, out) return out def _report(self, lo, hi, reach, start, out): if lo >= hi: # @line L1 return mid = (lo + hi) // 2 if not reach(self.max[mid]): # @line L1 return self._report(lo, mid, reach, start, out) # @line L2 j = self.order[mid] l, r = self.iv[j] if not start(l): # @line L3 return if reach(r): # @line L4 out.append(j) self._report(mid + 1, hi, reach, start, out) # @line L5 # SEARCH: one matching interval, or None. def search(self, reach: Callable, start: Callable) -> Optional[int]: lo, hi = 0, len(self.order) # @line L6 while lo < hi: # @line L7 mid = (lo + hi) // 2 j = self.order[mid] l, r = self.iv[j] if reach(r) and start(l): # @line L7 return j left_mid = (lo + mid) // 2 if lo < mid and reach(self.max[left_mid]): # @line L8 hi = mid else: lo = mid + 1 return None # @line L9 def overlap(self, a, b, kind="closed") -> list[int]: return self.report(*overlap_tests(a, b, kind)) def stab(self, p, kind="closed") -> list[int]: return self.report(*stab_tests(p, kind)) def interval_tree_search(intervals: Sequence[Interval], a, b, kind="closed") -> list[int]: """Indices of the intervals that meet the query , by building the tree once.""" return IntervalTreeSearch(intervals).overlap(a, b, kind)