"""Interval merge utilities. Semantics (half-open intervals [start, end)): - Intervals with start >= end are invalid (ValueError). - Two intervals overlap/touch if a.start <= b.end and b.start <= a.end? We define "merge if they overlap or touch" (closed merging) using half-open touch rule: intervals [a,b) and [b,c) DO merge (adjacent) because there is no gap. intervals [a,b) and [b+1,c) do NOT merge. - Output is sorted, disjoint, merged, each as (start, end) tuple. - Empty input -> []. """ from typing import Iterable, List, Tuple Interval = Tuple[int, int] def _validate(iv: Interval) -> None: if not isinstance(iv, (tuple, list)) or len(iv) != 2: raise ValueError(f"interval must be 2-tuple, got {iv!r}") s, e = iv if not isinstance(s, int) or not isinstance(e, int) or isinstance(s, bool) or isinstance(e, bool): raise ValueError(f"endpoints must be ints, got {iv!r}") if s >= e: raise ValueError(f"empty/invalid interval {iv!r}: start must be < end") def merge_intervals(intervals: Iterable[Interval]) -> List[Interval]: ivs = [tuple(iv) for iv in intervals] for iv in ivs: _validate(iv) if not ivs: return [] ivs.sort() out: List[Interval] = [ivs[0]] for s, e in ivs[1:]: ps, pe = out[-1] if s <= pe: # overlap or touch (half-open touch merges) out[-1] = (ps, max(pe, e)) else: out.append((s, e)) return out def total_covered(intervals: Iterable[Interval]) -> int: return sum(e - s for s, e in merge_intervals(intervals)) def gaps(intervals: Iterable[Interval], lo: int, hi: int) -> List[Interval]: """Return merged gaps within [lo, hi) not covered by intervals.""" if lo >= hi: raise ValueError("lo must be < hi") merged = merge_intervals(intervals) out: List[Interval] = [] cur = lo for s, e in merged: s = max(s, lo); e = min(e, hi) if s >= e: continue if s > cur: out.append((cur, s)) cur = max(cur, e) if cur >= hi: break if cur < hi: out.append((cur, hi)) return out