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 lazybool revbit 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:
- 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)$.
- 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$!
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.
Invariant & State
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$! Callingmergeon 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'srand()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_64orSplitMix64).