Sqrt tree in Python
Static structure answering associative range queries in O(1) with O(n log log n) space.
- File
sqrt_tree.py, 115 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/sqrt-tree/. The authors are listed in prov/RBM9RY.jsonl.
from collections.abc import Callable, Sequence
from typing import Generic, TypeVar
T = TypeVar("T")
class SqrtTree(Generic[T]):
"""A static sqrt tree for an associative op.
The index space is [0, 2^K) with 2^K >= n. Layer j has segments of 2^k[j]
items, cut into blocks of 2^b[j] items with b[j] = ceil(k[j] / 2), and
k[j + 1] = b[j]. Each layer stores, for every index, the product from the
start of its block (pre) and to the end of its block (suf), clipped to n,
and for every segment the products of all runs of whole blocks (btw).
Building takes O(n log log n) time and space, and query(l, r) applies op
at most twice. op need not be commutative or idempotent.
"""
def __init__(self, values: Sequence[T], op: Callable[[T, T], T]) -> None:
self.a = list(values)
self.op = op
n = self.n = len(self.a)
big_k = max(0, (n - 1).bit_length()) # @line L1
self.ks: list[int] = []
self.bs: list[int] = []
k = big_k
while k >= 2:
self.ks.append(k)
self.bs.append((k + 1) // 2)
k = (k + 1) // 2
self.layer_of = [-1] * max(1, big_k)
for j, (k, b) in enumerate(zip(self.ks, self.bs)): # @line L2
for h in range(b, k):
self.layer_of[h] = j
self.pre: list[list[T]] = []
self.suf: list[list[T]] = []
self.btw: list[dict[tuple[int, int], T]] = []
for k, b in zip(self.ks, self.bs):
self._build_layer(k, b)
def _build_layer(self, k: int, b: int) -> None:
n, a, op = self.n, self.a, self.op
size = 1 << b
pre: list[T] = list(a)
suf: list[T] = list(a)
for s in range(0, n, size):
e = min(n, s + size)
for i in range(s + 1, e): # @line L3
pre[i] = op(pre[i - 1], a[i])
for i in range(e - 2, s - 1, -1): # @line L4
suf[i] = op(a[i], suf[i + 1])
btw: dict[tuple[int, int], T] = {}
m = 1 << (k - b) # blocks per segment
nblocks = (n + size - 1) // size
for first in range(0, nblocks, m):
last = min(nblocks, first + m)
for p in range(first, last):
acc = suf[p * size] # @line L5
btw[(p, p)] = acc
for q in range(p + 1, last): # @line L6
acc = op(acc, suf[q * size])
btw[(p, q)] = acc
self.pre.append(pre)
self.suf.append(suf)
self.btw.append(btw)
def __len__(self) -> int:
return self.n
def query(self, l: int, r: int) -> T:
"""Return A[l] op ... op A[r] with at most two applications of op."""
if not 0 <= l <= r < self.n:
raise IndexError("bad range")
if l == r: # @line L7
return self.a[l]
h = (l ^ r).bit_length() - 1 # @line L8
if h == 0:
return self.op(self.a[l], self.a[r])
j = self.layer_of[h] # @line L9
b = self.bs[j]
p, q = l >> b, r >> b
if q == p + 1: # @line L10
return self.op(self.suf[j][l], self.pre[j][r])
mid = self.btw[j][(p + 1, q - 1)] # @line L11
return self.op(self.op(self.suf[j][l], mid), self.pre[j][r])
def check_invariant(self) -> None:
"""Check the layer plan and every stored product against a direct fold."""
a, op, n = self.a, self.op, self.n
def fold(x: int, y: int) -> T:
res = a[x]
for t in range(x + 1, y + 1):
res = op(res, a[t])
return res
big_k = max(0, (n - 1).bit_length())
assert n <= (1 << big_k)
for h in range(1, big_k):
j = self.layer_of[h]
assert self.bs[j] <= h < self.ks[j], h
for j, (k, b) in enumerate(zip(self.ks, self.bs)):
assert 2 * b >= k > b >= 1
size = 1 << b
for i in range(n):
s = (i >> b) << b
e = min(n, s + size) - 1
assert self.pre[j][i] == fold(s, i), (j, i)
assert self.suf[j][i] == fold(i, e), (j, i)
for (p, q), v in self.btw[j].items():
assert p >> (k - b) == q >> (k - b) and p <= q
assert v == fold(p * size, min(n, (q + 1) * size) - 1), (j, p, q)
test_sqrt_tree.py 84 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/sqrt-tree/. The authors are listed in prov/RBM9RY.jsonl.
import math
import operator
import random
import pytest
from sqrt_tree import SqrtTree
A = [5, 2, 8, 1, 9, 3, 7, 4, 6, 0, 2, 5, 1, 8, 3, 7]
def test_example() -> None:
t = SqrtTree(A, operator.add)
assert t.ks == [4, 2] and t.bs == [2, 1]
assert t.layer_of == [-1, 1, 0, 0]
assert t.suf[0][1] == 11 and t.pre[0][14] == 12
assert t.btw[0][(1, 2)] == 36
assert t.query(1, 14) == 59
assert t.query(2, 7) == 32
assert t.query(5, 6) == 10
assert t.query(8, 9) == 6
assert t.query(0, 15) == 71
t.check_invariant()
def test_edges() -> None:
with pytest.raises(IndexError):
SqrtTree([], operator.add).query(0, 0)
one = SqrtTree([4], operator.add)
assert one.query(0, 0) == 4
one.check_invariant()
two = SqrtTree([4, 5], operator.add)
assert two.query(0, 1) == 9
with pytest.raises(IndexError):
two.query(1, 0)
def test_counts_combines() -> None:
calls = [0]
def op(x: int, y: int) -> int:
calls[0] += 1
return x + y
t = SqrtTree(list(range(300)), op)
for l in range(0, 300, 7):
for r in range(l, 300, 11):
calls[0] = 0
assert t.query(l, r) == sum(range(l, r + 1))
assert calls[0] <= 2
def test_random_against_brute_force() -> None:
rng = random.Random(7)
ops = [(operator.add, 0), (min, None), (math.gcd, None)]
cases = 0
for _ in range(60):
n = rng.randint(1, 140)
vals = [rng.randint(-50, 50) for _ in range(n)]
op, _ = rng.choice(ops)
t = SqrtTree(vals, op)
t.check_invariant()
for _ in range(12):
l = rng.randrange(n)
r = rng.randrange(l, n)
want = vals[l]
for x in vals[l + 1 : r + 1]:
want = op(want, x)
assert t.query(l, r) == want
cases += 1
for _ in range(30):
n = rng.randint(1, 90)
words = [rng.choice("abc") for _ in range(n)]
t = SqrtTree(words, operator.add) # string concatenation is not commutative
t.check_invariant()
for _ in range(10):
l = rng.randrange(n)
r = rng.randrange(l, n)
assert t.query(l, r) == "".join(words[l : r + 1])
cases += 1
assert cases >= 500
What the next level needs
- the tests did not run in this build