Algorithm Search⌘K

Dynamic (sparse) segment tree in Python

A segment tree that creates nodes only when needed, supporting queries over huge index ranges with memory proportional to the updates.

File
dynamic_segment_tree.py, 91 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
dynamic_segment_tree.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dynamic-segment-tree/. The authors are listed in prov/6P1XHV.jsonl.


class DynamicSegmentTree:
    """Range sums over A[0..U-1] of integers, all zero at the start.

    Nodes are made only on the paths of updates. update and query take
    O(log U) time; after m updates the tree has at most 1 + m*ceil(lg U)
    nodes. Node 0 means "absent" and node 1 is the root.
    """

    def __init__(self, universe: int) -> None:
        """Make the tree of U = universe zeros; requires universe >= 1."""
        if universe < 1:
            raise ValueError("universe must be at least 1")
        self.universe = universe
        self.val = [0, 0]
        self.left = [0, 0]
        self.right = [0, 0]

    def _new_node(self) -> int:
        self.val.append(0)
        self.left.append(0)
        self.right.append(0)
        return len(self.val) - 1

    def node_count(self) -> int:
        """The number of created nodes, the root included."""
        return len(self.val) - 1

    def update(self, i: int, x: int) -> None:
        """Set A[i] += x, for 0 <= i < U, in O(log U) time."""
        if not 0 <= i < self.universe:
            raise IndexError("index out of range")
        v, lo, hi = 1, 0, self.universe  # @line L1
        while True:  # @line L2
            self.val[v] += x  # @line L3
            if hi - lo == 1:  # @line L4
                return
            mid = (lo + hi) // 2  # @line L5
            if i < mid:  # @line L6
                if self.left[v] == 0:  # @line L7
                    c = self._new_node()
                    self.left[v] = c
                v, hi = self.left[v], mid  # @line L8
            else:  # @line L9
                if self.right[v] == 0:  # @line L10
                    c = self._new_node()
                    self.right[v] = c
                v, lo = self.right[v], mid  # @line L11

    def query(self, l: int, r: int) -> int:
        """Return A[l] + ... + A[r-1] for 0 <= l <= r <= U, in O(log U) time."""
        if not 0 <= l <= r <= self.universe:
            raise IndexError("range out of bounds")
        total = 0
        stack = [(1, 0, self.universe)]
        while stack:
            v, lo, hi = stack.pop()
            if v == 0 or r <= lo or hi <= l:  # @line L12
                continue
            if l <= lo and hi <= r:  # @line L13
                total += self.val[v]
                continue
            mid = (lo + hi) // 2  # @line L14
            stack.append((self.right[v], mid, hi))  # @line L15
            stack.append((self.left[v], lo, mid))
        return total

    def check_invariant(self) -> None:
        """Check that each inner node holds the sum of its children and
        that each node is reached exactly once from the root."""
        seen = [False] * len(self.val)
        stack = [(1, 0, self.universe)]
        while stack:
            v, lo, hi = stack.pop()
            assert not seen[v], "node reached twice"
            seen[v] = True
            if hi - lo == 1:
                assert self.left[v] == 0 and self.right[v] == 0, "leaf with children"
                continue
            mid = (lo + hi) // 2
            s = 0
            for c, a, b in ((self.left[v], lo, mid), (self.right[v], mid, hi)):
                if c != 0:
                    s += self.val[c]
                    stack.append((c, a, b))
            if self.left[v] or self.right[v]:
                assert self.val[v] == s, "inner sum differs from the children"
        assert all(seen[1:]), "unreachable node"
test_dynamic_segment_tree.py 69 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dynamic-segment-tree/. The authors are listed in prov/6P1XHV.jsonl.

import random

import pytest

from dynamic_segment_tree import DynamicSegmentTree


def test_example() -> None:
    t = DynamicSegmentTree(16)
    assert t.node_count() == 1
    t.update(3, 5)
    assert t.node_count() == 5
    t.update(10, 2)
    assert t.node_count() == 9
    t.update(11, 4)
    assert t.node_count() == 10
    assert t.val[1] == 11
    assert t.query(2, 12) == 11
    assert t.query(0, 8) == 5
    assert t.query(4, 10) == 0
    assert t.node_count() == 10
    t.check_invariant()


def test_edges() -> None:
    t = DynamicSegmentTree(1)
    assert t.query(0, 1) == 0
    t.update(0, -3)
    assert t.query(0, 1) == -3 and t.query(0, 0) == 0 and t.query(1, 1) == 0
    assert t.node_count() == 1
    with pytest.raises(IndexError):
        t.update(1, 1)
    with pytest.raises(IndexError):
        t.query(1, 0)
    with pytest.raises(ValueError):
        DynamicSegmentTree(0)
    big = DynamicSegmentTree(10**18)
    big.update(10**18 - 1, 7)
    big.update(0, 1)
    assert big.query(0, 10**18) == 8
    assert big.query(1, 10**18 - 1) == 0
    assert big.node_count() <= 1 + 2 * 60
    big.check_invariant()


def test_random_against_brute_force() -> None:
    rng = random.Random(31337)
    for _ in range(500):
        u = rng.randint(1, 70)
        t = DynamicSegmentTree(u)
        a = [0] * u
        m = 0
        for _ in range(rng.randint(0, 40)):
            if rng.random() < 0.5:
                i = rng.randrange(u)
                x = rng.randint(-9, 9)
                t.update(i, x)
                a[i] += x
                m += 1
            else:
                l = rng.randint(0, u)
                r = rng.randint(l, u)
                assert t.query(l, r) == sum(a[l:r])
        bound = 1 + m * max(0, (u - 1).bit_length())
        assert t.node_count() <= min(bound, 2 * u - 1)
        t.check_invariant()

What the next level needs

  • the tests did not run in this build

Read the full page about Dynamic (sparse) segment tree