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
# 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