diff --git a/wizard-work/beam/weighted_interval_scheduling.py b/wizard-work/beam/weighted_interval_scheduling.py new file mode 100644 index 0000000..56bb327 --- /dev/null +++ b/wizard-work/beam/weighted_interval_scheduling.py @@ -0,0 +1,88 @@ +"""Weighted Interval Scheduling via dynamic programming. + +Given intervals (start, end, weight), choose a subset of mutually +non-overlapping intervals maximizing total weight. Intervals overlap when +their time ranges intersect with positive length; touching endpoints +([a,b] and [b,c]) are allowed to coexist. + +Algorithm: + sort intervals by end time + p[i] = largest index j < i with end_j <= start_i (predecessor) + OPT[i] = max(OPT[i-1], weight_i + OPT[p[i]]) + answer OPT[n] + +Predecessor search uses binary search on sorted end-times: O(n log n). +""" +from __future__ import annotations + +import bisect +from typing import List, Tuple + + +Interval = Tuple[int, int, int] # (start, end, weight) + + +def weighted_interval_scheduling(intervals: List[Interval]) -> int: + if not intervals: + return 0 + # sort by end time; keep original weights + iv = sorted(intervals, key=lambda x: x[1]) + n = len(iv) + ends = [iv[i][1] for i in range(n)] + # dp[i] = best using first i intervals (1-indexed); dp[0]=0 + dp = [0] * (n + 1) + for i in range(1, n + 1): + s, e, w = iv[i - 1] + # predecessor: largest j (0..i-1) with ends[j-1] <= s -> index j in 1-based terms + # search in ends[0:i-1] + j = bisect.bisect_right(ends, s, 0, i - 1) # count of ends <= s among first i-1 + # j is count => predecessor dp index j + take = w + dp[j] + skip = dp[i - 1] + dp[i] = take if take > skip else skip + return dp[n] + + +def brute_force(intervals: List[Interval]) -> int: + """Exhaustive enumeration for verification on small inputs.""" + n = len(intervals) + best = 0 + for mask in range(1 << n): + chosen = [intervals[k] for k in range(n) if mask & (1 << k)] + ok = True + for a in range(len(chosen)): + for b in range(a + 1, len(chosen)): + # overlap with positive length + s1, e1, _ = chosen[a] + s2, e2, _ = chosen[b] + lo = max(s1, s2) + hi = min(e1, e2) + if hi - lo > 0: + ok = False + break + if not ok: + break + if ok: + best = max(best, sum(w for _, _, w in chosen)) + return best + + +if __name__ == "__main__": + # self-test with randomized comparison + import random + random.seed(42) + for trial in range(2000): + n = random.randint(0, 8) + ivs = [] + for _ in range(n): + s = random.randint(0, 6) + length = random.randint(1, 4) + e = s + length + w = random.randint(1, 9) + ivs.append((s, e, w)) + a = weighted_interval_scheduling(ivs) + b = brute_force(ivs) + if a != b: + raise SystemExit(f"MISMATCH trial={trial} got={a} exp={b} ivs={ivs}") + print("OK: 2000 randomized trials passed") + print("example:", weighted_interval_scheduling([(1, 3, 5), (2, 5, 6), (4, 6, 5), (6, 7, 4)]))