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