Algorithm Search⌘K

Two-dimensional Fenwick tree in Python

Fenwick tree nested over two dimensions for point updates and rectangle sums in O(log^2 n).

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

from collections.abc import Sequence


def _low(i: int) -> int:
    """Return the lowest set bit of i > 0, written g(i) on the page."""
    return i & -i


class FenwickTree2D:
    """A two-dimensional Fenwick tree over an n x m integer matrix.

    The table f is 1-indexed with f[0][*] and f[*][0] unused. The cell f[i][j]
    holds the sum of M over the rows (i - g(i), i] and the columns
    (j - g(j), j], in 1-indexed form, where g(i) is the lowest set bit of i.
    Build takes O(nm) time; add, prefix and query take O(log n log m) time.
    Public methods use 0-indexed rows and columns.
    """

    def __init__(self, matrix: Sequence[Sequence[int]]) -> None:
        self.n = len(matrix)
        self.m = len(matrix[0]) if self.n else 0
        n, m = self.n, self.m
        f = [[0] * (m + 1) for _ in range(n + 1)]
        for i in range(1, n + 1):  # @line L1
            row = matrix[i - 1]
            if len(row) != m:
                raise ValueError("all rows must have the same length")
            for j in range(1, m + 1):
                f[i][j] = row[j - 1]
        for i in range(1, n + 1):  # @line L2
            for j in range(1, m + 1):
                p = j + _low(j)
                if p <= m:  # @line L3
                    f[i][p] += f[i][j]
        for i in range(1, n + 1):  # @line L4
            p = i + _low(i)
            if p <= n:  # @line L5
                fp, fi = f[p], f[i]
                for j in range(1, m + 1):
                    fp[j] += fi[j]
        self.f = f

    def add(self, r: int, c: int, delta: int) -> None:
        """Add delta to M[r][c], 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")
        i = r + 1  # @line L6
        while i <= self.n:  # @line L7
            j = c + 1  # @line L8
            row = self.f[i]
            while j <= self.m:  # @line L9
                row[j] += delta  # @line L10
                j += _low(j)  # @line L11
            i += _low(i)  # @line L12

    def prefix(self, r: int, c: int) -> int:
        """Return the sum of M over the rows 0..r-1 and the columns 0..c-1."""
        if not (0 <= r <= self.n and 0 <= c <= self.m):
            raise IndexError("prefix out of range")
        s = 0  # @line L13
        i = r
        while i > 0:  # @line L14
            j = c  # @line L15
            row = self.f[i]
            while j > 0:  # @line L16
                s += row[j]  # @line L17
                j -= _low(j)  # @line L18
            i -= _low(i)  # @line L19
        return s  # @line L20

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

        An empty rectangle (r1 = r2 + 1 or c1 = c2 + 1) gives 0.
        """
        if not (0 <= r1 <= r2 + 1 <= self.n and 0 <= c1 <= c2 + 1 <= self.m):
            raise IndexError("rectangle out of range")
        return (  # @line L21
            self.prefix(r2 + 1, c2 + 1)
            - self.prefix(r1, c2 + 1)
            - self.prefix(r2 + 1, c1)
            + self.prefix(r1, c1)
        )

    def to_matrix(self) -> list[list[int]]:
        """Return the current matrix M, in O(nm log n log m) time."""
        return [
            [self.query(r, c, r, c) for c in range(self.m)] for r in range(self.n)
        ]

    def check_invariant(self) -> None:
        """Assert that each cell holds the sum of its rectangle of M."""
        mat = self.to_matrix()
        for i in range(1, self.n + 1):
            for j in range(1, self.m + 1):
                want = sum(
                    mat[a][b]
                    for a in range(i - _low(i), i)
                    for b in range(j - _low(j), j)
                )
                assert self.f[i][j] == want, (i, j)
test_two_dimensional_fenwick_tree.py 80 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/two-dimensional-fenwick-tree/. The authors are listed in prov/RNZB8E.jsonl.

import random

import pytest

from two_dimensional_fenwick_tree import FenwickTree2D

M = [[2, 1, 4, 3], [0, 5, 2, 1], [3, 1, 6, 2], [1, 4, 0, 5]]


def brute(mat: list[list[int]], r1: int, c1: int, r2: int, c2: int) -> int:
    return sum(mat[r][c] for r in range(r1, r2 + 1) for c in range(c1, c2 + 1))


def test_example() -> None:
    ft = FenwickTree2D(M)
    assert ft.f[1][1:] == [2, 3, 4, 10]
    assert ft.f[2][1:] == [2, 8, 6, 18]
    assert ft.f[3][1:] == [3, 4, 6, 12]
    assert ft.f[4][1:] == [6, 17, 12, 40]
    assert ft.prefix(3, 4) == 30
    assert ft.query(1, 1, 2, 3) == 17
    assert ft.query(0, 0, 3, 3) == 40
    assert ft.query(2, 0, 3, 1) == 9
    ft.add(2, 1, 3)
    assert ft.f[3][1:] == [3, 7, 6, 15]
    assert ft.f[4][1:] == [6, 20, 12, 43]
    assert ft.query(1, 1, 2, 3) == 20
    ft.check_invariant()


def test_single_cell_and_empty() -> None:
    ft = FenwickTree2D([[7]])
    assert ft.query(0, 0, 0, 0) == 7
    assert ft.query(0, 0, -1, 0) == 0
    ft.add(0, 0, -10)
    assert ft.query(0, 0, 0, 0) == -3
    empty = FenwickTree2D([])
    assert empty.prefix(0, 0) == 0


def test_one_row_and_one_column() -> None:
    row = FenwickTree2D([[5, 2, 8, 1, 9, 3, 7, 4]])
    assert row.query(0, 2, 0, 6) == 28
    col = FenwickTree2D([[5], [2], [8], [1], [9], [3], [7], [4]])
    assert col.query(2, 0, 6, 0) == 28
    row.check_invariant()
    col.check_invariant()


def test_errors() -> None:
    ft = FenwickTree2D(M)
    with pytest.raises(IndexError):
        ft.add(4, 0, 1)
    with pytest.raises(IndexError):
        ft.query(0, 0, 4, 0)
    with pytest.raises(ValueError):
        FenwickTree2D([[1, 2], [3]])


def test_random_against_brute_force() -> None:
    rng = random.Random(20261104)
    for _ in range(60):
        n, m = rng.randint(1, 9), rng.randint(1, 9)
        mat = [[rng.randint(-20, 20) for _ in range(m)] for _ in range(n)]
        ft = FenwickTree2D(mat)
        ft.check_invariant()
        for _ in range(30):
            if rng.random() < 0.4:
                r, c, d = rng.randrange(n), rng.randrange(m), rng.randint(-9, 9)
                mat[r][c] += d
                ft.add(r, c, d)
            else:
                r1, r2 = sorted((rng.randrange(n), rng.randrange(n)))
                c1, c2 = sorted((rng.randrange(m), rng.randrange(m)))
                assert ft.query(r1, c1, r2, c2) == brute(mat, r1, c1, r2, c2)
        ft.check_invariant()
        assert ft.to_matrix() == mat

What the next level needs

  • the tests did not run in this build

Read the full page about Two-dimensional Fenwick tree