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