Algorithm Search⌘K

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
sqrt_tree.pyRawGitHub
# 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

Read the full page about Sqrt tree