DSA DSA In Depth
0%
lesson treapimplicit-treaprandomized-bst

Randomized Treaps & Implicit Treaps: Rebalancing Without Rotations via split and merge

7 min read Module 3.2: Self-Balancing, Augmented, Disk-Backed & Persistent Trees Updated Oct 23, 2026
📄 Cheat Sheet
AlgorithmsThread 9: Treaps (Split & Merge Paradigm)
SecondThread AlgorithmsThread 9: Treaps (Split & Merge Paradigm) Watch on YouTube ↗

Matt Fontaine demonstrates how split and merge replace rotations and how Implicit Treaps support O(log N) subarray reversal and range cuts.

0:00 / --:--

The split and merge Paradigm & Implicit Treaps (Dynamic Rope Arrays)

Instead of bottom-up rotations, every Treap operation reduces to two recursive primitives that run in $O(\log N)$ expected time:

  • split(T, key) -> (L, R): Partitions Treap $T$ into two valid Treaps: $L$ (all nodes with key $\le \text{key}$) and $R$ (all nodes with key $> \text{key}$). If $T.\text{key} \le \text{key}$, then $T$ and all of $T.\text{left}$ belong to $L$; we recursively split $T.\text{right}$ into $(T.\text{right}, R)$ and return $(T, R)$. Symmetric when $T.\text{key} > \text{key}$.
  • merge(L, R) -> T: Combines two Treaps $L$ and $R$ under the precondition that every key in $L$ is strictly $\le$ every key in $R$ ($max(L) \le \min(R)$). Whichever root has the higher random priority ($L.\text{prio} > R.\text{prio}$) becomes the combined root, and we recursively merge its right child with $R$ (or $L$ with $R.\text{left}$).

With split and merge, every dictionary and sequence operation is a 2-line composition:

  • insert(T, newNode): (L, R) = split(T, newNode.key); return merge(merge(L, newNode), R);
  • erase(T, key): (L, M) = split(T, key - 1); (Target, R) = split(M, key); return merge(L, R);
  • Implicit Treap (Index-Keyed Sequence / Rope): Drop explicit keys altogether! Maintain only subtree sizes $\text{sz}(u) = 1 + \text{sz}(u.\text{left}) + \text{sz}(u.\text{right})$. Now a node's 0-based array index in the current subtree is simply $\text{sz}(u.\text{left})$! Calling splitBySize(T, k) cuts an array of length $N$ at position $k$ in $O(\log N)$ time, enabling $O(\log N)$ arbitrary range cut-and-paste, range reversal (via a lazy bool rev bit that swaps children), and range sum/min queries!
Treap / Implicit Treap Operation Expected Time High-Probability Bound Equivalent split/merge Composition
split(T, k) / merge(L, R) O(log N) $\le c \log_2 N$ w.p. $1 - N^{-c}$ Fundamental recursive primitives
insert(k) / erase(k) O(log N) $\le c \log_2 N$ w.p. $1 - N^{-c}$ 1 split + 2 merges (or 2 splits + 1 merge)
Implicit Array Range Reverse / Cut-Paste $[l, r]$ O(log N) $\le c \log_2 N$ w.p. $1 - N^{-c}$ Split into $[0..l-1], [l..r], [r+1..N-1]$, toggle lazy bit, merge!
Set Union / Intersection of Two Treaps $(M \le N)$ O(M log(N/M)) Optimal information-theoretic bound Divide-and-conquer split by higher-priority root

Cartesian Tree Uniqueness Theorem (Aragon & Seidel, 1989)

Given $N$ nodes $(k_i, p_i)$ with distinct keys $k_i$ and distinct priorities $p_i$, why is the Treap topology 100% independent of insertion order? Because:

  1. By the Max-Heap property on priorities, the root of any non-empty subset $S$ must be the unique element $u^* = \arg\max_{u \in S} p(u)$.
  2. By the BST property on keys, the left subtree must contain $S_{<} = \{u \in S : k(u) < k(u^*)\}$ and the right subtree must contain $S_{>} = \{u \in S : k(u) > k(u^*)\}$.

By induction on $|S|$, the tree is uniquely determined and is identical to inserting the keys in decreasing order of $p_i$. Since $p_i$ are independent uniform random variables, their rank order is a uniformly random permutation $\pi \in S_N$—forcing $\mathbb{E}[\text{depth}] = 2 \ln N \approx 1.386 \log_2 N$ even when an attacker inserts keys in sorted order $1, 2, \dots, N$!

Randomized Treap (split / merge) & Splay Tree Zig-Zig Animator
Expected / Amortized O(log N)

Inspect how a Treap simultaneously satisfies BST order on keys and Max-Heap order on random priorities via two primitives — split(T, k) and merge(L, R) — and how Splay Tree zig-zig rotations halve path depth.

Step 1 / 9
Custom Input:
Timeline

Invariant & State

Treap Root = (key=20, prio=95): BST invariant on keys (10 < 20 < 25 < 30 < 40) ∧ Max-Heap on priorities (95 > 81 > 60).
🌳 Live 2D Hierarchical Tree Topology — Randomized Treap (split / merge) & Splay Tree Zig-Zig Animator Visited/Active Pivot/Root Resolved
10 (p… [0] 20 (p… [1] 25 (p… [2] 30 (p… [3] 40 (p… [4]
[0]10 (p=42)
[1]20 (p=95)
[2]25 (p=60)
[3]30 (p=81)
[4]40 (p=35)
Primitive
split(T, k=25)
Recursion Depth
1 (O(log N))
L Keys (≤ 25)
Building...
R Keys (> 25)
Building...
[Step 1] Start split(root=20, k=25): Since root.key (20) ≤ 25, root 20 and its entire left subtree [10] belong to Left Output Tree L.
Treap Node (Key | Priority): BST on Key, Max-Heap on Priority 10|95 4|80 15|72 2|40 7|61 ╌╌ split(T, key = 6) → (L: keys ≤ 6, R: keys > 6) • At (10|95): 10 > 6 ⇒ (10|95) and right child go to R; recursively split left child (4|80) at key = 6 • At (4|80): 4 ≤ 6 ⇒ (4|80) and left child go to L; recursively split right child (7|61) at key = 6 Implicit Treap: replace key with leftSize = sz(u.left) ⇒ O(log N) Subarray Cut/Paste/Reverse on dynamic arrays!
flowchart TD M["merge(L, R) Precondition: max(L) <= min(R)"] --> Base{"L == null OR R == null ?"} Base -- "Yes" --> RetBase["return L ? L : R"] Base -- "No" --> Prio{"L.prio > R.prio ?"} Prio -- "Yes (L wins root)" --> LWin["L.right = merge(L.right, R) pull(L); return L"] Prio -- "No (R wins root)" --> RWin["R.left = merge(L, R.left) pull(R); return R"]

Hardware Reality: Fast PRNGs (SplitMix64 / xorshift64*) Inside Node Constructors

Never call std::random_device (which traps into the OS kernel /dev/urandom or RDRAND) or instantiate a $2.5\text{ KB}$ std::mt19937 state object per node! A single static 64-bit SplitMix64 or xorshift64* state variable generates uniformly distributed 64-bit priorities in 1.2 nanoseconds (3 ALU instructions: shift, XOR, multiply) with zero branches and zero probability of priority collisions for $N le 10^8$.

Implementation: Implicit Treap with $O(\log N)$ Range Reversal (split + merge)

// C++20: Implicit Treap supporting O(log N) Subarray Reversal [l, r] via split & merge
#include <cstdint>
#include <utility>

struct ImplicitTreapNode {
    int val;
    uint64_t prio;
    int sz = 1;
    bool rev = false; // Lazy range-reversal tag
    ImplicitTreapNode *l = nullptr, *r = nullptr;

    ImplicitTreapNode(int v, uint64_t p) : val(v), prio(p) {}
};

inline int size(const ImplicitTreapNode* t) noexcept { return t ? t->sz : 0; }

inline void pull(ImplicitTreapNode* t) noexcept {
    if (t) t->sz = 1 + size(t->l) + size(t->r);
}

inline void pushLazy(ImplicitTreapNode* t) noexcept {
    if (t && t->rev) {
        std::swap(t->l, t->r);
        if (t->l) t->l->rev ^= true;
        if (t->r) t->r->rev ^= true;
        t->rev = false;
    }
}

// Splits T into L (first k elements) and R (remaining N - k elements)
std::pair<ImplicitTreapNode*, ImplicitTreapNode*> splitBySize(ImplicitTreapNode* t, int k) {
    if (!t) return {nullptr, nullptr};
    pushLazy(t);
    int leftCount = size(t->l);
    if (k <= leftCount) {
        auto [L, R] = splitBySize(t->l, k);
        t->l = R;
        pull(t);
        return {L, t};
    } else {
        auto [L, R] = splitBySize(t->r, k - leftCount - 1);
        t->r = L;
        pull(t);
        return {t, R};
    }
}

ImplicitTreapNode* merge(ImplicitTreapNode* L, ImplicitTreapNode* R) {
    if (!L || !R) return L ? L : R;
    pushLazy(L);
    pushLazy(R);
    if (L->prio > R->prio) {
        L->r = merge(L->r, R);
        pull(L);
        return L;
    } else {
        R->l = merge(L, R->l);
        pull(R);
        return R;
    }
}

// Reverses contiguous subarray [l, r] (0-indexed inclusive) in O(log N) expected time
ImplicitTreapNode* reverseRange(ImplicitTreapNode* root, int l, int r) {
    auto [t1, t23] = splitBySize(root, l);
    auto [t2, t3]  = splitBySize(t23, r - l + 1);
    if (t2) t2->rev ^= true;
    return merge(t1, merge(t2, t3));
}

Failure Modes & Adversarial Pitfalls

  • Violating merge(L, R) Precondition ($max(L) le min(R)$): merge(L, R) is NOT a general set-union operation—it strictly requires that every key in $L$ is $le$ every key in $R$! Calling merge on overlapping key ranges silently destroys the BST invariant. (To union two overlapping Treaps in $O(M log(N/M))$, split the lower-priority root's tree by the higher-priority root's key and recursively union both sides.)
  • Using 16-Bit std::rand() on Windows (RAND_MAX = 32767): On MSVC/Windows, C's rand() only returns a 15-bit integer ($0 dots 32767$). In a Treap of $N = 10^5$ nodes, thousands of nodes collide on the exact same priority, causing subtree degeneration! Always use a 64-bit PRNG (mt19937_64 or SplitMix64).
Check yourself Question 1 of 4
Concept Check Dual Invariants & Uniqueness of a Treap recall

Given a set of $N$ items where all search keys $k_i$ are distinct and all randomly assigned priorities $p_i$ are distinct, how many valid Treap topologies exist for this exact set of $(k_i, p_i)$ pairs?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Evaluate the exact asymptotic expected number of rotations per Treap insertion:

Key Takeaways

A Treap assigns each node a random priority and enforces BST order on keys and Max-Heap order on priorities, guaranteeing the exact distribution of a Randomly Built BST regardless of input order.
Every Treap operation can be implemented without rotations using two O(log N) primitives: split(T, key) and merge(L, R) (where max(L) <= min(R)).
An Implicit Treap replaces explicit keys with dynamic subtree sizes sz(u.left), turning a balanced BST into a dynamic array supporting O(log N) insert, delete, cut-and-paste, and range reversal.
Always generate priorities with a fast 64-bit PRNG (e.g., SplitMix64 or mt19937_64) to prevent birthday-paradox priority collisions.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue