Algorithm Search⌘K

Implicit treap in Python

A treap keyed by position in the sequence rather than by stored key, supporting insertion, deletion, reversal and range operations on arrays.

File
implicit_treap.py, 184 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
implicit_treap.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/implicit-treap/. The authors are listed in prov/47AZT5.jsonl.

from __future__ import annotations

import random
from typing import Iterator


class Node:
    """A treap node keyed by position. size and total describe the subtree;
    rev means that the subtree must still be mirrored (a lazy reversal)."""

    __slots__ = ("value", "prio", "left", "right", "size", "total", "rev")

    def __init__(self, value: int, prio: float) -> None:
        self.value = value
        self.prio = prio
        self.left: Node | None = None
        self.right: Node | None = None
        self.size = 1
        self.total = value
        self.rev = False


def size(t: Node | None) -> int:
    return 0 if t is None else t.size


def total(t: Node | None) -> int:
    return 0 if t is None else t.total


def push(t: Node) -> None:
    """Apply a pending reversal at t and pass it to the children. O(1)."""
    if t.rev:
        t.left, t.right = t.right, t.left
        for c in (t.left, t.right):
            if c is not None:
                c.rev = not c.rev
        t.rev = False


def update(t: Node) -> None:
    """Recompute size and total of t from its children. O(1)."""
    t.size = size(t.left) + size(t.right) + 1
    t.total = total(t.left) + total(t.right) + t.value


def split(t: Node | None, i: int) -> tuple[Node | None, Node | None]:
    """Split t into its first i items and the rest. O(depth)."""
    if t is None:  # @line L1
        return None, None
    push(t)  # @line L2
    if size(t.left) < i:  # @line L3
        a, b = split(t.right, i - size(t.left) - 1)
        t.right = a
        update(t)
        return t, b
    a, b = split(t.left, i)  # @line L4
    t.left = b
    update(t)
    return a, t


def merge(a: Node | None, b: Node | None) -> Node | None:
    """Concatenate a and b (all of a comes first). O(depth)."""
    if a is None or b is None:  # @line L5
        return a if b is None else b
    if a.prio > b.prio:  # @line L6
        push(a)
        a.right = merge(a.right, b)
        update(a)
        return a
    push(b)  # @line L7
    b.left = merge(a, b.left)
    update(b)
    return b


class ImplicitTreap:
    """A list of integers kept as an implicit treap.

    Positions start at 0. insert, delete, get, reverse and range_sum take
    O(log n) expected time; the priorities come from rng (or are given).
    """

    def __init__(self, rng: random.Random | None = None) -> None:
        self.root: Node | None = None
        self.rng = rng if rng is not None else random.Random()

    def __len__(self) -> int:
        return size(self.root)

    def insert(self, i: int, x: int, prio: float | None = None) -> None:
        """Insert x before position i (0 <= i <= n)."""
        if not 0 <= i <= len(self):
            raise IndexError(i)
        p = self.rng.random() if prio is None else prio
        a, b = split(self.root, i)  # @line L8
        self.root = merge(merge(a, Node(x, p)), b)

    def append(self, x: int, prio: float | None = None) -> None:
        self.insert(len(self), x, prio)

    def delete(self, i: int) -> int:
        """Remove and return the item at position i."""
        if not 0 <= i < len(self):
            raise IndexError(i)
        a, b = split(self.root, i)  # @line L9
        m, c = split(b, 1)
        assert m is not None
        self.root = merge(a, c)
        return m.value

    def reverse(self, lo: int, hi: int) -> None:
        """Reverse the items at positions lo .. hi - 1."""
        if not 0 <= lo <= hi <= len(self):
            raise IndexError((lo, hi))
        a, b = split(self.root, lo)  # @line L10
        m, c = split(b, hi - lo)
        if m is not None:
            m.rev = not m.rev  # @line L11
        self.root = merge(a, merge(m, c))

    def range_sum(self, lo: int, hi: int) -> int:
        """Return the sum of the items at positions lo .. hi - 1."""
        if not 0 <= lo <= hi <= len(self):
            raise IndexError((lo, hi))
        a, b = split(self.root, lo)  # @line L12
        m, c = split(b, hi - lo)
        s = total(m)
        self.root = merge(a, merge(m, c))
        return s

    def get(self, i: int) -> int:
        """Return the item at position i, walking down one path."""
        if not 0 <= i < len(self):
            raise IndexError(i)
        t = self.root
        while t is not None:
            push(t)
            s = size(t.left)
            if i < s:
                t = t.left
            elif i > s:
                i -= s + 1
                t = t.right
            else:
                return t.value
        raise AssertionError("unreachable")

    def __iter__(self) -> Iterator[int]:
        stack: list[Node] = []
        t = self.root
        while stack or t is not None:
            while t is not None:
                push(t)
                stack.append(t)
                t = t.left
            t = stack.pop()
            yield t.value
            t = t.right

    def to_list(self) -> list[int]:
        return list(self)

    def height(self) -> int:
        def h(t: Node | None) -> int:
            return -1 if t is None else 1 + max(h(t.left), h(t.right))
        return h(self.root)

    def check_invariant(self) -> None:
        """Assert the heap order on priorities and the sizes and totals."""
        def go(t: Node | None) -> None:
            if t is None:
                return
            for c in (t.left, t.right):
                assert c is None or c.prio <= t.prio, "heap order"
            assert t.size == size(t.left) + size(t.right) + 1, "size"
            assert t.total == total(t.left) + total(t.right) + t.value, "total"
            go(t.left)
            go(t.right)
        go(self.root)
test_implicit_treap.py 111 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/implicit-treap/. The authors are listed in prov/47AZT5.jsonl.

import math
import random

import pytest

from implicit_treap import ImplicitTreap, merge, push, split


def shape(t):
    if t is None:
        return None
    push(t)
    if t.left is None and t.right is None:
        return t.value
    return (shape(t.left), t.value, shape(t.right))


def page_treap():
    t = ImplicitTreap()
    for v, p in zip([10, 20, 30, 40, 50], [3, 5, 1, 4, 2]):
        t.append(v, p)
    return t


def test_page_example():
    t = page_treap()
    assert shape(t.root) == (10, 20, (30, 40, 50))
    a, b = split(t.root, 1)
    assert shape(a) == 10 and shape(b) == (None, 20, (30, 40, 50))
    m, c = split(b, 3)
    assert shape(m) == (None, 20, (30, 40, None)) and shape(c) == 50
    t.root = merge(a, merge(m, c))
    t.reverse(1, 4)
    assert t.root.value == 20 and not t.root.rev  # the flag was pushed by merge
    assert t.to_list() == [10, 40, 30, 20, 50]
    assert shape(t.root) == ((10, 40, 30), 20, 50)
    t.insert(2, 25, 6)
    assert shape(t.root) == ((10, 40, None), 25, (30, 20, 50))
    assert t.range_sum(1, 4) == 95
    assert t.delete(0) == 10 and t.to_list() == [40, 25, 30, 20, 50]
    t.check_invariant()


def test_edge_cases():
    t = ImplicitTreap(random.Random(3))
    assert len(t) == 0 and t.to_list() == [] and t.range_sum(0, 0) == 0
    t.reverse(0, 0)
    with pytest.raises(IndexError):
        t.get(0)
    with pytest.raises(IndexError):
        t.insert(1, 5)
    t.insert(0, 5)
    t.reverse(0, 1)
    assert t.get(0) == 5 and t.delete(0) == 5 and t.root is None
    assert split(None, 3) == (None, None) and merge(None, None) is None


def test_double_reverse_cancels():
    t = ImplicitTreap(random.Random(8))
    for v in range(100):
        t.append(v)
    t.reverse(10, 90)
    t.reverse(10, 90)
    assert t.to_list() == list(range(100))
    t.reverse(0, 100)
    assert t.to_list() == list(range(99, -1, -1))
    t.check_invariant()


def test_expected_height():
    t = ImplicitTreap(random.Random(1997))
    n = 20000
    for v in range(n):
        t.append(v)
    assert t.height() <= 5 * math.log(n)
    assert [t.get(i) for i in range(0, n, 997)] == list(range(0, n, 997))


def test_random_against_list():
    rng = random.Random(1989)
    for case in range(600):
        t = ImplicitTreap(random.Random(case))
        ref: list[int] = []
        for _ in range(rng.randint(0, 60)):
            op = rng.random()
            n = len(ref)
            if op < 0.35 or n == 0:
                i, x = rng.randint(0, n), rng.randint(-50, 50)
                t.insert(i, x)
                ref.insert(i, x)
            elif op < 0.5:
                i = rng.randrange(n)
                assert t.delete(i) == ref.pop(i)
            elif op < 0.7:
                lo = rng.randint(0, n)
                hi = rng.randint(lo, n)
                t.reverse(lo, hi)
                ref[lo:hi] = ref[lo:hi][::-1]
            elif op < 0.85:
                lo = rng.randint(0, n)
                hi = rng.randint(lo, n)
                assert t.range_sum(lo, hi) == sum(ref[lo:hi])
            else:
                i = rng.randrange(n)
                assert t.get(i) == ref[i]
            assert len(t) == len(ref)
        t.check_invariant()
        assert t.to_list() == ref

What the next level needs

  • the tests did not run in this build

Read the full page about Implicit treap