Algorithm Search⌘K

Two-dimensional segment tree in Python

A segment tree of segment trees that answers and updates rectangle queries over a matrix in logarithmic squared time.

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

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

T = TypeVar("T")


class SegmentTree2D(Generic[T]):
    """A segment tree over rows whose nodes are segment trees over columns.

    Outer node vx covers a range of rows and inner node vy a range of
    columns, both numbered as in a binary heap (root 1, children 2v, 2v + 1).
    t[vx][vy] holds the op-product of M over the rows of vx and the columns of
    vy. op must be associative and commutative with the neutral element
    identity. Build takes O(nm) time; query and point assignment take
    O(log n log m) time.
    """

    def __init__(
        self,
        matrix: Sequence[Sequence[T]],
        op: Callable[[T, T], T] = operator.add,  # type: ignore[assignment]
        identity: T = 0,  # type: ignore[assignment]
    ) -> None:
        self.n = len(matrix)
        self.m = len(matrix[0]) if self.n else 0
        if self.n == 0 or self.m == 0:
            raise ValueError("the matrix must have at least one row and one column")
        for row in matrix:
            if len(row) != self.m:
                raise ValueError("all rows must have the same length")
        self.op = op
        self.e = identity
        self.mat = [list(row) for row in matrix]
        self.t: list[list[T]] = [
            [identity] * (4 * self.m) for _ in range(4 * self.n)
        ]
        self._build_x(1, 0, self.n - 1)

    # ----- build -----

    def _build_y(self, vx: int, lx: int, rx: int, vy: int, ly: int, ry: int) -> None:
        t = self.t
        if ly == ry:  # @line L1
            if lx == rx:  # @line L2
                t[vx][vy] = self.mat[lx][ly]
            else:
                t[vx][vy] = self.op(t[2 * vx][vy], t[2 * vx + 1][vy])
            return  # @line L3
        my = (ly + ry) // 2  # @line L4
        self._build_y(vx, lx, rx, 2 * vy, ly, my)
        self._build_y(vx, lx, rx, 2 * vy + 1, my + 1, ry)
        t[vx][vy] = self.op(t[vx][2 * vy], t[vx][2 * vy + 1])  # @line L5

    def _build_x(self, vx: int, lx: int, rx: int) -> None:
        if lx != rx:  # @line L6
            mx = (lx + rx) // 2
            self._build_x(2 * vx, lx, mx)
            self._build_x(2 * vx + 1, mx + 1, rx)
        self._build_y(vx, lx, rx, 1, 0, self.m - 1)  # @line L7

    # ----- query -----

    def _query_y(self, vx: int, vy: int, ly: int, ry: int, c1: int, c2: int) -> T:
        if c2 < ly or ry < c1:  # @line L8
            return self.e
        if c1 <= ly and ry <= c2:  # @line L9
            return self.t[vx][vy]
        my = (ly + ry) // 2  # @line L10
        return self.op(
            self._query_y(vx, 2 * vy, ly, my, c1, c2),
            self._query_y(vx, 2 * vy + 1, my + 1, ry, c1, c2),
        )

    def _query_x(
        self, vx: int, lx: int, rx: int, r1: int, r2: int, c1: int, c2: int
    ) -> T:
        if r2 < lx or rx < r1:  # @line L11
            return self.e
        if r1 <= lx and rx <= r2:  # @line L12
            return self._query_y(vx, 1, 0, self.m - 1, c1, c2)
        mx = (lx + rx) // 2  # @line L13
        return self.op(
            self._query_x(2 * vx, lx, mx, r1, r2, c1, c2),
            self._query_x(2 * vx + 1, mx + 1, rx, r1, r2, c1, c2),
        )

    def query(self, r1: int, c1: int, r2: int, c2: int) -> T:
        """Return the op-product of M[r][c] for r1 <= r <= r2, c1 <= c <= c2.

        An empty rectangle (r1 = r2 + 1 or c1 = c2 + 1) gives the identity.
        """
        if not (0 <= r1 <= r2 + 1 <= self.n and 0 <= c1 <= c2 + 1 <= self.m):
            raise IndexError("rectangle out of range")
        if r1 > r2 or c1 > c2:
            return self.e
        return self._query_x(1, 0, self.n - 1, r1, r2, c1, c2)

    # ----- update -----

    def _update_y(
        self, vx: int, lx: int, rx: int, vy: int, ly: int, ry: int, c: int
    ) -> None:
        t = self.t
        if ly == ry:  # @line L14
            if lx == rx:  # @line L15
                t[vx][vy] = self.mat[lx][ly]
            else:
                t[vx][vy] = self.op(t[2 * vx][vy], t[2 * vx + 1][vy])
            return  # @line L16
        my = (ly + ry) // 2  # @line L17
        if c <= my:
            self._update_y(vx, lx, rx, 2 * vy, ly, my, c)
        else:
            self._update_y(vx, lx, rx, 2 * vy + 1, my + 1, ry, c)
        t[vx][vy] = self.op(t[vx][2 * vy], t[vx][2 * vy + 1])  # @line L18

    def _update_x(self, vx: int, lx: int, rx: int, r: int, c: int) -> None:
        if lx != rx:  # @line L19
            mx = (lx + rx) // 2
            if r <= mx:
                self._update_x(2 * vx, lx, mx, r, c)
            else:
                self._update_x(2 * vx + 1, mx + 1, rx, r, c)
        self._update_y(vx, lx, rx, 1, 0, self.m - 1, c)  # @line L20

    def update(self, r: int, c: int, x: T) -> None:
        """Set M[r][c] = x, for 0 <= r < n and 0 <= c < m."""
        if not (0 <= r < self.n and 0 <= c < self.m):
            raise IndexError("cell out of range")
        self.mat[r][c] = x
        self._update_x(1, 0, self.n - 1, r, c)

    # ----- checks -----

    def _fold(self, r1: int, r2: int, c1: int, c2: int) -> T:
        acc = self.e
        for r in range(r1, r2 + 1):
            for c in range(c1, c2 + 1):
                acc = self.op(acc, self.mat[r][c])
        return acc

    def check_invariant(self) -> None:
        """Assert that each used cell holds the product of its rectangle."""

        def rows(vx: int, lx: int, rx: int) -> None:
            cols(vx, lx, rx, 1, 0, self.m - 1)
            if lx != rx:
                mx = (lx + rx) // 2
                rows(2 * vx, lx, mx)
                rows(2 * vx + 1, mx + 1, rx)

        def cols(vx: int, lx: int, rx: int, vy: int, ly: int, ry: int) -> None:
            assert self.t[vx][vy] == self._fold(lx, rx, ly, ry), (vx, vy)
            if ly != ry:
                my = (ly + ry) // 2
                cols(vx, lx, rx, 2 * vy, ly, my)
                cols(vx, lx, rx, 2 * vy + 1, my + 1, ry)

        rows(1, 0, self.n - 1)
test_two_dimensional_segment_tree.py 81 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/two-dimensional-segment-tree/. The authors are listed in prov/QYYQT1.jsonl.

import math
import random

import pytest

from two_dimensional_segment_tree import SegmentTree2D

M = [[2, 1, 4, 3], [0, 5, 2, 1], [3, 1, 6, 2], [1, 4, 0, 5]]
INF = math.inf


def test_example_min() -> None:
    st = SegmentTree2D(M, min, INF)
    assert st.t[1][1] == 0
    assert st.t[5][5] == 5
    assert st.t[5][3] == 1
    assert st.t[6][5] == 1
    assert st.t[6][3] == 2
    assert st.query(1, 1, 2, 3) == 1
    st.update(2, 1, 7)
    assert st.t[6][5] == 7
    assert st.t[3][5] == 4
    assert st.query(1, 1, 2, 3) == 1
    st.update(1, 3, 9)
    assert st.t[5][3] == 2
    assert st.query(1, 1, 2, 3) == 2
    st.check_invariant()


def test_example_sum() -> None:
    st = SegmentTree2D(M)
    assert st.query(1, 1, 2, 3) == 17
    assert st.query(0, 0, 3, 3) == 40
    st.update(2, 1, 4)
    assert st.query(1, 1, 2, 3) == 20
    st.check_invariant()


def test_edges() -> None:
    one = SegmentTree2D([[7]], min, INF)
    assert one.query(0, 0, 0, 0) == 7
    assert one.query(0, 0, -1, 0) == INF
    row = SegmentTree2D([[5, 2, 8, 1, 9, 3, 7, 4]], max, -INF)
    assert row.query(0, 2, 0, 6) == 9
    col = SegmentTree2D([[5], [2], [8], [1], [9]], math.gcd, 0)
    assert col.query(0, 0, 4, 0) == 1
    with pytest.raises(ValueError):
        SegmentTree2D([])
    with pytest.raises(ValueError):
        SegmentTree2D([[1, 2], [3]])
    with pytest.raises(IndexError):
        row.update(1, 0, 3)
    with pytest.raises(IndexError):
        row.query(0, 0, 0, 8)


@pytest.mark.parametrize("name", ["sum", "min", "max"])
def test_random_against_brute_force(name: str) -> None:
    op, e = {"sum": (lambda a, b: a + b, 0), "min": (min, INF), "max": (max, -INF)}[name]
    rng = random.Random(7 + len(name))
    for _ in range(40):
        n, m = rng.randint(1, 7), rng.randint(1, 7)
        mat = [[rng.randint(-30, 30) for _ in range(m)] for _ in range(n)]
        st = SegmentTree2D(mat, op, e)
        for _ in range(25):
            if rng.random() < 0.4:
                r, c, x = rng.randrange(n), rng.randrange(m), rng.randint(-30, 30)
                mat[r][c] = x
                st.update(r, c, x)
            else:
                r1, r2 = sorted((rng.randrange(n), rng.randrange(n)))
                c1, c2 = sorted((rng.randrange(m), rng.randrange(m)))
                want = e
                for r in range(r1, r2 + 1):
                    for c in range(c1, c2 + 1):
                        want = op(want, mat[r][c])
                assert st.query(r1, c1, r2, c2) == want
        st.check_invariant()

What the next level needs

  • the tests did not run in this build

Read the full page about Two-dimensional segment tree