beam: add SparseTable (O(1) idempotent range query) + randomized tests
This commit is contained in:
1 parent
5be1bb2e84
commit
ea525f92a0
2 files changed
+112
No files matched your search
@@ -0,0 +1,45 @@
|
||||
"""Sparse table for O(1) range queries over an idempotent associative op.
|
||||
|
||||
Idempotent means f(x, x) == x (min, max, gcd, bitwise AND/OR). For such ops
|
||||
the overlap trick gives O(1) queries after O(n log n) preprocessing.
|
||||
|
||||
Usage:
|
||||
st = SparseTable([3,1,4,1,5,9,2,6])
|
||||
st.query(1, 4) # min of positions 1..4 (inclusive, 0-indexed) -> 1
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import math
|
||||
from typing import Callable, Sequence
|
||||
|
||||
|
||||
class SparseTable:
|
||||
def __init__(self, data: Sequence, func: Callable = min):
|
||||
self.n = len(data)
|
||||
self.func = func
|
||||
self._log = [0] * (self.n + 1)
|
||||
for i in range(2, self.n + 1):
|
||||
self._log[i] = self._log[i // 2] + 1
|
||||
self.k = self._log[self.n] if self.n > 0 else 0
|
||||
# st[j][i] = func over data[i .. i + 2^j - 1]
|
||||
self.st = []
|
||||
if self.n > 0:
|
||||
self.st.append(list(data))
|
||||
j = 1
|
||||
while (1 << j) <= self.n:
|
||||
prev = self.st[j - 1]
|
||||
span = 1 << (j - 1)
|
||||
cur = [func(prev[i], prev[i + span]) for i in range(self.n - (1 << j) + 1)]
|
||||
self.st.append(cur)
|
||||
j += 1
|
||||
|
||||
def query(self, l: int, r: int):
|
||||
"""Inclusive range [l, r], 0-indexed. Raises on empty/invalid."""
|
||||
if self.n == 0:
|
||||
raise IndexError("empty table")
|
||||
if l < 0 or r >= self.n or l > r:
|
||||
raise IndexError(f"invalid range [{l}, {r}] for n={self.n}")
|
||||
j = self._log[r - l + 1]
|
||||
return self.func(self.st[j][l], self.st[j][r - (1 << j) + 1])
|
||||
|
||||
def __len__(self):
|
||||
return self.n
|
||||
@@ -0,0 +1,67 @@
|
||||
import random
|
||||
import unittest
|
||||
from sparse_table import SparseTable
|
||||
|
||||
|
||||
class TestSparseTable(unittest.TestCase):
|
||||
def test_min_basic(self):
|
||||
a = [3, 1, 4, 1, 5, 9, 2, 6]
|
||||
st = SparseTable(a)
|
||||
self.assertEqual(st.query(0, 7), 1)
|
||||
self.assertEqual(st.query(1, 4), 1)
|
||||
self.assertEqual(st.query(2, 5), 1)
|
||||
self.assertEqual(st.query(6, 7), 2)
|
||||
|
||||
def test_single(self):
|
||||
st = SparseTable([42])
|
||||
self.assertEqual(st.query(0, 0), 42)
|
||||
|
||||
def test_empty_raises(self):
|
||||
st = SparseTable([])
|
||||
with self.assertRaises(IndexError):
|
||||
st.query(0, 0)
|
||||
|
||||
def test_invalid_range(self):
|
||||
st = SparseTable([1, 2, 3])
|
||||
for l, r in [(-1, 1), (0, 3), (2, 1)]:
|
||||
with self.assertRaises(IndexError):
|
||||
st.query(l, r)
|
||||
|
||||
def test_max_func(self):
|
||||
a = [3, 1, 4, 1, 5, 9, 2, 6]
|
||||
st = SparseTable(a, func=max)
|
||||
self.assertEqual(st.query(0, 7), 9)
|
||||
self.assertEqual(st.query(1, 4), 5)
|
||||
|
||||
def test_gcd_func(self):
|
||||
import math
|
||||
a = [12, 18, 24, 30]
|
||||
st = SparseTable(a, func=math.gcd)
|
||||
self.assertEqual(st.query(0, 3), 6)
|
||||
self.assertEqual(st.query(1, 2), 6)
|
||||
|
||||
def test_fuzz_min(self):
|
||||
rng = random.Random(7)
|
||||
for _ in range(200):
|
||||
n = rng.randint(1, 60)
|
||||
a = [rng.randint(-100, 100) for _ in range(n)]
|
||||
st = SparseTable(a)
|
||||
for _ in range(50):
|
||||
l = rng.randrange(n)
|
||||
r = rng.randrange(l, n)
|
||||
self.assertEqual(st.query(l, r), min(a[l:r + 1]))
|
||||
|
||||
def test_fuzz_max(self):
|
||||
rng = random.Random(9)
|
||||
for _ in range(100):
|
||||
n = rng.randint(1, 40)
|
||||
a = [rng.randint(-50, 50) for _ in range(n)]
|
||||
st = SparseTable(a, func=max)
|
||||
for _ in range(30):
|
||||
l = rng.randrange(n)
|
||||
r = rng.randrange(l, n)
|
||||
self.assertEqual(st.query(l, r), max(a[l:r + 1]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user