Algorithm Search⌘K

Bender-Farach-Colton RMQ in Python

Linear preprocessing and constant-time RMQ by reduction to LCA with block-based sparse tables.

File
bender_farach_colton_rmq.py, 155 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
bender_farach_colton_rmq.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/bender-farach-colton-rmq/. The authors are listed in prov/WNYTAQ.jsonl.

from typing import Sequence


class BenderFarachColtonRMQ:
    """Range minimum queries in O(1) after O(n) preprocessing.

    The array is turned into its Cartesian tree, the tree into the depth
    sequence of an Euler tour (adjacent depths differ by one), and that
    sequence is cut into blocks of size about (lg m) / 2 for m = 2n - 1.
    A sparse table over the block minima answers spans of whole blocks, and
    one table per block shape answers queries inside a block. query(l, r)
    returns the position of the leftmost minimum of a[l..r].
    """

    def __init__(self, a: Sequence[int], block: int = 0) -> None:
        """Preprocess a; block <= 0 picks max(1, floor(lg m / 2))."""
        self.a = list(a)
        n = len(self.a)
        self.n = n
        # L1: Cartesian tree with the leftmost minimum at the root.
        parent = [-1] * n
        left = [-1] * n
        right = [-1] * n
        stack: list[int] = []
        for i in range(n):  # @line L1
            last = -1
            while stack and self.a[stack[-1]] > self.a[i]:
                last = stack.pop()
            if last != -1:
                left[i] = last
                parent[last] = i
            if stack:
                right[stack[-1]] = i
                parent[i] = stack[-1]
            stack.append(i)
        self.root = stack[0] if stack else -1
        self.parent = parent
        # L2: Euler tour of node ids, their depths and first occurrences.
        self.euler: list[int] = []
        self.depth: list[int] = []
        self.first = [0] * n
        if n:
            # state 0: first visit, 1: the left child is done, 2: both are done
            work = [(self.root, 0, 0)]
            while work:  # @line L2
                v, d, st = work.pop()
                if st == 0:
                    self.first[v] = len(self.euler)
                self.euler.append(v)
                self.depth.append(d)
                if st == 0 and left[v] != -1:
                    work.append((v, d, 1))
                    work.append((left[v], d + 1, 0))
                elif st <= 1 and right[v] != -1:
                    work.append((v, d, 2))
                    work.append((right[v], d + 1, 0))
        m = len(self.depth)
        self.m = m
        b = block if block > 0 else max(1, (max(m, 1).bit_length() - 1) // 2)  # @line L3
        self.b = b
        nb = (m + b - 1) // b
        self.block_min: list[int] = []
        self.block_shape: list[tuple[int, int]] = []
        self.tables: dict[tuple[int, int], list[list[int]]] = {}
        for j in range(nb):  # @line L4
            s, e = j * b, min(m, (j + 1) * b)
            best = s
            sig = 0
            for k in range(s + 1, e):
                if self.depth[k] < self.depth[best]:
                    best = k
                if self.depth[k] > self.depth[k - 1]:
                    sig |= 1 << (k - s - 1)
            self.block_min.append(best)
            shape = (e - s, sig)
            self.block_shape.append(shape)
            if shape not in self.tables:  # @line L6
                self.tables[shape] = self._shape_table(*shape)
        # L5: sparse table over the block minima, storing positions in depth.
        self.sparse: list[list[int]] = [list(self.block_min)]
        h = 1
        while 2 * h <= nb:  # @line L5
            prev = self.sparse[-1]
            row = []
            for j in range(nb - 2 * h + 1):
                x, y = prev[j], prev[j + h]
                row.append(x if self.depth[x] <= self.depth[y] else y)
            self.sparse.append(row)
            h *= 2

    @staticmethod
    def _shape_table(length: int, sig: int) -> list[list[int]]:
        """table[i][k] = offset of the leftmost minimum of offsets i..k."""
        d = [0] * length
        for k in range(1, length):
            d[k] = d[k - 1] + (1 if sig >> (k - 1) & 1 else -1)
        table = [[0] * length for _ in range(length)]
        for i in range(length):
            best = i
            for k in range(i, length):
                if d[k] < d[best]:
                    best = k
                table[i][k] = best
        return table

    def _in_block(self, j: int, x: int, y: int) -> int:
        s = j * self.b
        return s + self.tables[self.block_shape[j]][x - s][y - s]

    def _better(self, x: int, y: int) -> int:
        return x if self.depth[x] <= self.depth[y] else y

    def _blocks(self, i: int, j: int) -> int:
        """Position of a minimum depth over the blocks i..j, i <= j."""
        k = (j - i + 1).bit_length() - 1
        row = self.sparse[k]
        return self._better(row[i], row[j - (1 << k) + 1])

    def query(self, l: int, r: int) -> int:
        """Position of the leftmost minimum of a[l..r], 0 <= l <= r < n."""
        if not 0 <= l <= r < self.n:
            raise IndexError(f"bad range [{l}, {r}] for length {self.n}")
        x, y = self.first[l], self.first[r]  # @line L7
        if x > y:
            x, y = y, x
        bx, by = x // self.b, y // self.b  # @line L8
        if bx == by:  # @line L9
            k = self._in_block(bx, x, y)
        else:  # @line L10
            k = self._better(self._in_block(bx, x, bx * self.b + self.b - 1), self._in_block(by, by * self.b, y))
            if bx + 1 <= by - 1:
                k = self._better(k, self._blocks(bx + 1, by - 1))
        return self.euler[k]  # @line L11

    def check_invariant(self) -> None:
        """Check the Cartesian tree, the Euler tour and the block data."""
        for v in range(self.n):
            p = self.parent[v]
            if p == -1:
                assert v == self.root
            else:
                assert self.a[p] <= self.a[v]
                assert self.a[p] < self.a[v] or p < v
        assert self.m == (2 * self.n - 1 if self.n else 0)
        for k in range(1, self.m):
            assert abs(self.depth[k] - self.depth[k - 1]) == 1
        for v in range(self.n):
            assert self.euler[self.first[v]] == v
        for j, best in enumerate(self.block_min):
            s, e = j * self.b, min(self.m, (j + 1) * self.b)
            assert self.depth[best] == min(self.depth[s:e])
        assert len(self.tables) <= 2 ** max(0, self.b - 1) + 1
test_bender_farach_colton_rmq.py 76 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/bender-farach-colton-rmq/. The authors are listed in prov/WNYTAQ.jsonl.

import random

import pytest

from bender_farach_colton_rmq import BenderFarachColtonRMQ


def leftmost_min(a: list[int], l: int, r: int) -> int:
    best = l
    for k in range(l + 1, r + 1):
        if a[k] < a[best]:
            best = k
    return best


def test_example() -> None:
    a = [4, 2, 5, 1, 6, 3]
    t = BenderFarachColtonRMQ(a, block=3)
    t.check_invariant()
    assert t.euler == [3, 1, 0, 1, 2, 1, 3, 5, 4, 5, 3]
    assert t.depth == [0, 1, 2, 1, 2, 1, 0, 1, 2, 1, 0]
    assert t.first == [2, 1, 4, 0, 8, 7]
    assert t.block_min == [0, 3, 6, 10]
    assert t.block_shape[0] == t.block_shape[2]
    assert len(t.tables) == 3
    assert t.query(0, 2) == 1
    assert t.query(2, 5) == 3
    assert t.query(4, 5) == 5
    assert t.query(0, 5) == 3
    assert t.query(4, 4) == 4


def test_edges() -> None:
    t = BenderFarachColtonRMQ([])
    t.check_invariant()
    with pytest.raises(IndexError):
        t.query(0, 0)
    t = BenderFarachColtonRMQ([7])
    t.check_invariant()
    assert t.query(0, 0) == 0
    t = BenderFarachColtonRMQ([2, 2, 2, 2])
    t.check_invariant()
    assert t.query(1, 3) == 1
    t = BenderFarachColtonRMQ(list(range(20, 0, -1)))
    assert t.query(0, 19) == 19
    with pytest.raises(IndexError):
        t.query(3, 2)


def test_random() -> None:
    rng = random.Random(11)
    for _ in range(600):
        n = rng.randint(1, 40)
        a = [rng.randint(0, 6) for _ in range(n)]
        block = rng.choice([0, 0, 1, 2, 3, 5])
        t = BenderFarachColtonRMQ(a, block=block)
        t.check_invariant()
        for _ in range(10):
            l = rng.randrange(n)
            r = rng.randrange(l, n)
            assert t.query(l, r) == leftmost_min(a, l, r)


def test_large_default_block() -> None:
    rng = random.Random(3)
    a = [rng.randint(0, 10**6) for _ in range(3000)]
    t = BenderFarachColtonRMQ(a)
    t.check_invariant()
    assert t.b == (len(t.depth).bit_length() - 1) // 2
    for _ in range(500):
        l = rng.randrange(3000)
        r = rng.randrange(l, 3000)
        assert t.query(l, r) == leftmost_min(a, l, r)

What the next level needs

  • the tests did not run in this build

Read the full page about Bender-Farach-Colton RMQ