Algorithm Search⌘K

Mo's algorithm with updates in Python

Adds time as a third sorting dimension so offline queries with point updates run in O(n^(5/3)).

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

from typing import Callable, Hashable, Sequence, TypeVar

T = TypeVar("T")
V = TypeVar("V")


def default_block(n: int) -> int:
    """The block size max(1, ceil(n^(2/3)))."""
    b = 1
    while b * b * b < n * n:
        b += 1
    return max(1, b)


def mo_with_updates(
    a: Sequence[V],
    queries: Sequence[tuple[int, int, int]],
    updates: Sequence[tuple[int, V]],
    add: Callable[[V], None],
    remove: Callable[[V], None],
    answer: Callable[[], T],
    block: int = 0,
) -> list[T]:
    """Answer offline window queries (l, r, t) on the array after t updates.

    A query (l, r, t) asks about a[l:r] after the first t updates of
    updates have been applied; an update (p, x) sets a[p] = x. add(x) and
    remove(x) put the value x into the window or take it out, and answer()
    reads the window. block <= 0 picks ceil(n^(2/3)).
    Cost: O(q B + n^2 / B + m n^2 / B^2) calls of add and remove for q
    queries and m updates, plus O(q log q) to sort.
    """
    arr = list(a)
    upd = [list(u) for u in updates]
    n, q = len(arr), len(queries)
    b = block if block > 0 else default_block(n)
    order = sorted(range(q), key=lambda j: (queries[j][0] // b, queries[j][1] // b, queries[j][2]))  # @line L2
    cur_l = cur_r = cur_t = 0  # @line L3
    ans: list[T] = [None] * q  # type: ignore[list-item]

    def apply(j: int) -> None:
        p = upd[j][0]  # @line L10
        inside = cur_l <= p < cur_r
        if inside:  # @line L11
            remove(arr[p])
        arr[p], upd[j][1] = upd[j][1], arr[p]  # @line L12
        if inside:  # @line L13
            add(arr[p])

    for j in order:  # @line L4
        l, r, t = queries[j]
        while cur_t < t:  # @line L5
            apply(cur_t)
            cur_t += 1
        while cur_t > t:  # @line L6
            cur_t -= 1
            apply(cur_t)
        while cur_l > l:  # @line L7
            cur_l -= 1
            add(arr[cur_l])
        while cur_r < r:
            add(arr[cur_r])
            cur_r += 1
        while cur_l < l:
            remove(arr[cur_l])
            cur_l += 1
        while cur_r > r:
            cur_r -= 1
            remove(arr[cur_r])
        ans[j] = answer()  # @line L8
    return ans  # @line L9


def distinct_counts_with_updates(a: Sequence[Hashable], ops: Sequence[tuple[str, int, object]]) -> list[int]:
    """Answers of ("Q", l, r) operations, the number of distinct values of
    a[l:r], interleaved with ("U", p, x) operations that set a[p] = x."""
    queries: list[tuple[int, int, int]] = []
    updates: list[tuple[int, Hashable]] = []
    for kind, u, w in ops:  # @line L1
        if kind == "U":
            if not 0 <= u < len(a):
                raise IndexError(f"update position {u} out of range")
            updates.append((u, w))  # type: ignore[arg-type]
        else:
            r = int(w)  # type: ignore[call-overload]
            if not 0 <= u <= r <= len(a):
                raise IndexError(f"bad range [{u}, {r})")
            queries.append((u, r, len(updates)))
    count: dict[Hashable, int] = {}
    distinct = [0]

    def add(x: Hashable) -> None:
        c = count.get(x, 0)
        if c == 0:
            distinct[0] += 1
        count[x] = c + 1

    def remove(x: Hashable) -> None:
        c = count[x] - 1
        count[x] = c
        if c == 0:
            distinct[0] -= 1

    return mo_with_updates(a, queries, updates, add, remove, lambda: distinct[0])
test_mo_algorithm_with_updates.py 67 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/mo-algorithm-with-updates/. The authors are listed in prov/XFPAYN.jsonl.

import random

import pytest

from mo_algorithm_with_updates import default_block, distinct_counts_with_updates, mo_with_updates


def brute(a: list[int], ops: list[tuple[str, int, int]]) -> list[int]:
    arr = list(a)
    out = []
    for kind, u, w in ops:
        if kind == "U":
            arr[u] = w
        else:
            out.append(len(set(arr[u:w])))
    return out


def test_example() -> None:
    a = [1, 2, 1, 3, 2]
    ops = [("Q", 0, 3), ("U", 1, 1), ("Q", 0, 3), ("Q", 1, 5), ("U", 3, 2), ("Q", 1, 5)]
    assert default_block(5) == 3
    assert distinct_counts_with_updates(a, ops) == [2, 1, 3, 2]


def test_moves_and_sum() -> None:
    a = [4, 1, 7]
    s = [0]

    def add(x: int) -> None:
        s[0] += x

    def remove(x: int) -> None:
        s[0] -= x

    queries = [(0, 3, 2), (0, 3, 0), (1, 2, 1), (2, 2, 2)]
    updates = [(1, 10), (0, 0)]
    assert mo_with_updates(a, queries, updates, add, remove, lambda: s[0], block=1) == [17, 12, 10, 0]
    assert a == [4, 1, 7]


def test_edges() -> None:
    assert distinct_counts_with_updates([], [("Q", 0, 0)]) == [0]
    assert distinct_counts_with_updates([5], [("U", 0, 5), ("Q", 0, 1)]) == [1]
    assert distinct_counts_with_updates([1, 2], []) == []
    with pytest.raises(IndexError):
        distinct_counts_with_updates([1], [("U", 1, 0)])
    with pytest.raises(IndexError):
        distinct_counts_with_updates([1], [("Q", 1, 0)])


def test_random() -> None:
    rng = random.Random(5)
    for _ in range(600):
        n = rng.randint(1, 12)
        a = [rng.randint(0, 4) for _ in range(n)]
        ops: list[tuple[str, int, int]] = []
        for _ in range(rng.randint(0, 15)):
            if rng.random() < 0.4:
                ops.append(("U", rng.randrange(n), rng.randint(0, 4)))
            else:
                l = rng.randint(0, n)
                ops.append(("Q", l, rng.randint(l, n)))
        assert distinct_counts_with_updates(a, ops) == brute(a, ops)

What the next level needs

  • the tests did not run in this build

Read the full page about Mo's algorithm with updates