Algorithm Search⌘K

Segment tree in Python

Binary tree over array segments answering range aggregates and point or range updates in O(log n).

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

import operator
from collections.abc import Callable, Sequence
from typing import Generic, TypeVar

T = TypeVar("T")


class SegmentTree(Generic[T]):
    """A recursive segment tree over a monoid (op, identity).

    Node 1 covers A[0 .. n-1]; node v with range [lo, hi] has the children
    2v (range [lo, mid]) and 2v + 1 (range [mid + 1, hi]), mid = (lo + hi) // 2.
    t[v] holds the product of A over the range of v. Building takes O(n) time,
    query and update take O(log n) time, and the list has fewer than 4n cells.
    op must be associative and identity must be its neutral element; op need
    not be commutative.
    """

    def __init__(
        self,
        values: Sequence[T],
        op: Callable[[T, T], T] = operator.add,  # type: ignore[assignment]
        identity: T = 0,  # type: ignore[assignment]
    ) -> None:
        self.n = len(values)
        self.op = op
        self.e = identity
        self.a = list(values)
        self.t: list[T] = [identity] * (4 * max(self.n, 1))
        if self.n:
            self._build(1, 0, self.n - 1)

    def _build(self, v: int, lo: int, hi: int) -> None:
        if lo == hi:  # @line L1
            self.t[v] = self.a[lo]
            return
        mid = (lo + hi) // 2  # @line L2
        self._build(2 * v, lo, mid)  # @line L3
        self._build(2 * v + 1, mid + 1, hi)  # @line L4
        self.t[v] = self.op(self.t[2 * v], self.t[2 * v + 1])  # @line L5

    def _query(self, v: int, lo: int, hi: int, l: int, r: int) -> T:
        if r < lo or hi < l:  # @line L6
            return self.e
        if l <= lo and hi <= r:  # @line L7
            return self.t[v]
        mid = (lo + hi) // 2  # @line L8
        return self.op(  # @line L9
            self._query(2 * v, lo, mid, l, r),
            self._query(2 * v + 1, mid + 1, hi, l, r),
        )

    def _update(self, v: int, lo: int, hi: int, i: int, x: T) -> None:
        if lo == hi:  # @line L10
            self.t[v] = x
            return
        mid = (lo + hi) // 2  # @line L11
        if i <= mid:  # @line L12
            self._update(2 * v, lo, mid, i, x)
        else:  # @line L13
            self._update(2 * v + 1, mid + 1, hi, i, x)
        self.t[v] = self.op(self.t[2 * v], self.t[2 * v + 1])  # @line L14

    def __len__(self) -> int:
        return self.n

    def query(self, l: int, r: int) -> T:
        """Return A[l] op ... op A[r], or the identity when l = r + 1.

        Precondition: 0 <= l <= r + 1 <= n. O(log n) time.
        """
        if not (0 <= l <= r + 1 <= self.n):
            raise IndexError("bad range")
        if l > r:
            return self.e
        return self._query(1, 0, self.n - 1, l, r)

    def update(self, i: int, x: T) -> None:
        """Set A[i] to x. Precondition: 0 <= i < n. O(log n) time."""
        if not 0 <= i < self.n:
            raise IndexError("index out of range")
        self.a[i] = x
        self._update(1, 0, self.n - 1, i, x)

    def check_invariant(self) -> None:
        """Check that each node holds the product of its range."""
        if not self.n:
            return
        stack = [(1, 0, self.n - 1)]
        while stack:
            v, lo, hi = stack.pop()
            want = self.a[lo]
            for k in range(lo + 1, hi + 1):
                want = self.op(want, self.a[k])
            assert self.t[v] == want, (v, lo, hi)
            if lo < hi:
                mid = (lo + hi) // 2
                stack.append((2 * v, lo, mid))
                stack.append((2 * v + 1, mid + 1, hi))
test_segment_tree.py 80 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/segment-tree/. The authors are listed in prov/P50MYS.jsonl.

import math
import random

import pytest

from segment_tree import SegmentTree


def test_example() -> None:
    st = SegmentTree([5, 2, 8, 1, 9, 3, 7, 4])
    assert st.t[1] == 39
    assert [st.t[2], st.t[3]] == [16, 23]
    assert [st.t[4], st.t[5], st.t[6], st.t[7]] == [7, 9, 12, 11]
    assert st.query(2, 6) == 28
    st.update(3, 6)
    assert [st.t[11], st.t[5], st.t[2], st.t[1]] == [6, 14, 21, 44]
    assert st.query(1, 4) == 25
    st.check_invariant()


def test_edges() -> None:
    empty: SegmentTree[int] = SegmentTree([])
    assert empty.query(0, -1) == 0
    one = SegmentTree([7])
    assert one.query(0, 0) == 7
    one.update(0, -3)
    assert one.query(0, 0) == -3
    same = SegmentTree([4] * 9, min, math.inf)
    assert same.query(2, 7) == 4
    assert same.query(5, 4) == math.inf
    big = SegmentTree([2**63 - 1, 2**63 - 1, -(2**63)])
    assert big.query(0, 2) == 2**63 - 2
    with pytest.raises(IndexError):
        one.query(0, 1)
    with pytest.raises(IndexError):
        one.update(1, 0)


def test_identity_for_min_is_not_zero() -> None:
    st = SegmentTree([5, 6, 7], min, math.inf)
    assert st.query(0, 2) == 5


def test_non_commutative() -> None:
    words = ["ab", "c", "", "de", "f", "gh", "i"]
    st = SegmentTree(words, lambda x, y: x + y, "")
    for l in range(len(words) + 1):
        for r in range(l - 1, len(words)):
            assert st.query(l, r) == "".join(words[l : r + 1])


def test_random_against_list() -> None:
    rng = random.Random(7)
    for _ in range(600):
        n = rng.randrange(1, 18)
        a = [rng.randrange(-50, 50) for _ in range(n)]
        kind = rng.randrange(3)
        if kind == 0:
            st = SegmentTree(a)
            agg = sum
        elif kind == 1:
            st = SegmentTree(a, min, math.inf)
            agg = lambda s: min(s, default=math.inf)  # noqa: E731
        else:
            a = [abs(x) for x in a]
            st = SegmentTree(a, math.gcd, 0)
            agg = lambda s: math.gcd(*s) if s else 0  # noqa: E731
        for _ in range(20):
            if rng.random() < 0.4:
                i = rng.randrange(n)
                a[i] = rng.randrange(0 if kind == 2 else -50, 50)
                st.update(i, a[i])
            else:
                l = rng.randrange(n + 1)
                r = rng.randrange(l - 1, n)
                assert st.query(l, r) == agg(a[l : r + 1])
        st.check_invariant()

What the next level needs

  • the tests did not run in this build

Read the full page about Segment tree