Implicit Treap
Key Points
- •An implicit treap is a randomized balanced binary tree that treats array positions as keys without storing them explicitly.
- •It supports expected O(log n) insertions and deletions at a position by splitting on subtree sizes and then merging.
- •Range operations like reverse, cut, paste, and cyclic shift are implemented by 2–3 splits and 1–2 merges.
- •The subtree size field lets you navigate by index: the inorder sequence is exactly the array order.
- •Lazy propagation lets you reverse a whole range in O(1) by toggling a flag and swapping children on push.
- •The expected height of an implicit treap is O(log n) because node priorities are random and form a random heap.
- •This data structure is the backbone of rope-like editors for large strings and competitive programming sequence problems.
- •Careful push and update functions are crucial to maintain size, sums, and lazy flags correctly.
Prerequisites
- →Binary Search Trees (BSTs) — Understanding tree structure, inorder traversal, and how rotations or structural changes affect order is fundamental.
- →Heap Property and Priority Queues — Treaps combine BST structure with heap ordering by priority; knowing heap invariants helps understand merge decisions.
- →Order-Statistic Trees — Implicit treaps navigate by index using subtree sizes, mirroring order-statistic operations.
- →Randomization and Expected Analysis — Expected O(log n) guarantees depend on random priorities; familiarity with expected-value reasoning is useful.
- →Lazy Propagation — Range operations like reverse are applied lazily to maintain efficiency and correctness.
- →Recursion and Call Stack — Split and merge are commonly implemented recursively; understanding stack behavior avoids mistakes.
- →Asymptotic Complexity (Big-O) — To analyze the logarithmic expected time and linear space of operations.
- →C++ Pointers and Dynamic Allocation — Treap nodes are heap-allocated and manipulated through raw pointers in typical contest implementations.
Detailed Explanation
Tap terms for definitions01Overview
An implicit treap is a balanced binary search tree that does not store actual keys. Instead, it treats each node’s position in the inorder traversal as its key. This makes it perfect for representing dynamic arrays where you often need to insert, delete, or modify elements by index. The “treap” name comes from combining a binary search tree (BST) organized by key with a heap organized by a random priority. In an implicit treap, the BST order is simply the inorder position, so we only need to maintain subtree sizes to move by index. Random priorities keep the tree balanced in expectation, yielding O(log n) time per operation. Core operations are split and merge: split(T, k) cuts the first k elements into one treap and the rest into another; merge(A, B) joins two treaps where all elements of A come before those of B. By chaining a few splits and merges, we can implement index-based insert, delete, reverse, range sum, substring extraction, concatenation, and cyclic shifts. With lazy propagation, we can apply certain range updates (like reversing a segment) in O(1) per operation by delaying work until the next necessary traversal. In practice, implicit treaps are simple to code, fast, and memory efficient. They are heavily used in competitive programming for sequence problems, serve as a foundation for rope data structures in text editors, and avoid the pitfalls of linked lists or expensive array shifts.
02Intuition & Analogies
Imagine a long row of books on a shelf, where the position of a book matters more than its title. You want to insert a new book at position 5, remove the book at position 10, flip the order of books between positions 20 and 30, or rotate a chunk of books to the right. Doing this on a plain array means shifting many books, which is slow. On a linked list, you can insert cheaply but it takes a long time to walk to position 10 every time. An implicit treap stores the books in a binary tree where an inorder walk visits them left-to-right exactly in shelf order. Each node also stores how many books are in its left and right subtrees (its size). That means to find the k-th book, you look at the size of the left side: if the left has L books, then the current node is the (L+1)-th; go left if k <= L, go right if k > L+1. This precisely mimics indexing while still being a tree. To keep the tree well-shaped (not a broom or a chain), every node gets a random “priority ticket.” Think of it as drawing a random number when the book is added. The tree maintains that higher-priority books sit closer to the root, forming a “heap” by priority. Randomness prevents long chains from occurring too often, so the height stays around log n most of the time. Now the magic operations: split at k literally cuts the shelf into two parts after the k-th book by following tree pointers, and merge stitches two shelves back together while respecting the random-priority heap rule. To reverse a segment, you cut the shelf into three parts [0..l-1], [l..r], [r+1..], flip the middle by toggling a flag (so its children are swapped lazily), then paste everything back together. Cyclic shifts cut the segment into two pieces at the right boundary and re-glue in swapped order.
03Formal Definition
04When to Use
Use an implicit treap when you maintain a dynamic sequence and need fast index-based operations: insert/delete at arbitrary positions, split a sequence, concatenate sequences, reverse a substring, rotate a subarray, or compute aggregates over ranges (sum, min, max) with optional lazy updates. This is especially valuable when operations are intermixed online and the sequence can grow to hundreds of thousands of elements. Practical domains include competitive programming problems about permutations and sequences; implementing rope-like string editors that frequently cut/paste or reverse substrings; simulation logs where events are inserted by position; and maintaining playlists or task lists with frequent rearrangements. Compared to std::vector, implicit treaps avoid O(n) shifts for middle insertions/deletions. Compared to linked lists, they support O(log n) random access by index. Compared to segment trees, they support structural edits (insert/delete) naturally. Compared to balanced BSTs with explicit keys, they simplify indexing by using subtree sizes and avoid key updates after insertions. If you only need static range queries/updates without structural edits, a Fenwick or segment tree may be simpler and faster. If you need string operations with persistent snapshots or disk-backed storage, specialized ropes or B-trees may be better. But for in-memory, online, mixed sequence edits with range transforms, implicit treaps are a strong default.
⚠️Common Mistakes
• Forgetting to push lazy flags before descending or rotating during split/merge. Always call push(node) before using its children; otherwise, reversed segments or pending updates corrupt order and aggregates. • Not updating size/sum/min/max after modifying children. Always call pull(node) (or update) after changing left or right pointers to recompute size and aggregated fields. • Off-by-one errors in split by size. Decide on 0-based or 1-based indices and stick to it. A common convention: split(T, k) puts the first k elements (0..k-1) in the left treap. • Using poor randomness or non-unique priorities. Prefer a high-quality generator like std::mt19937 and allow 32–64-bit priorities. Equal priorities can degrade balancing if you impose a biased tiebreak. • Neglecting k normalization for rotations: use k' = ((k % len) + len) % len to handle negative k and k ≥ len. • Integer overflow in aggregates (like sum). Store sums as long long and be mindful of constraints. • Excess recursion depth if the RNG is degenerate. While expected height is O(log n), a worst-case height of O(n) is possible; ensure your platform’s recursion limit is safe or convert to iterative where necessary. • Memory leaks in contest code. While often ignored in short runs, consider destructors or pooled allocators for production or stress environments.
Key Formulas
Subtree Size
Explanation: The size of a node’s subtree equals one for the node itself plus the sizes of its left and right subtrees. This is the core quantity used to navigate by index.
Inorder Position
Explanation: The position of v equals how many nodes are to its left: nodes in its own left subtree plus nodes to the left of every ancestor where v lies in the ancestor’s right subtree. This formalizes index computation.
Expected Height
Explanation: For i.i.d. random priorities, the expected height of a treap on n nodes grows proportionally to log n. This underpins expected O(log n) operations.
Split Recurrence
Explanation: A split follows one branch (left or right) plus constant work at each step, so its cost is proportional to the tree height. The expected cost is logarithmic.
Merge Recurrence
Explanation: Similarly, merge descends a single path determined by priorities, doing constant work per node, leading to expected logarithmic time.
Lazy Reverse
Explanation: To apply a pending reverse on a node, swap its children and toggle their reverse flags, then clear the current flag. Aggregates like sum remain correct because reversal only reorders.
Subtree Sum
Explanation: Maintains the sum of values in each subtree, enabling O(1) query after isolating a range with splits. Use 64-bit types to avoid overflow.
Normalized Rotation
Explanation: When rotating a segment of length L by k, normalize k into [0, L) to handle negative and large rotations. A rotation by 0 is a no-op.
Rotate by Splits/Merges
Explanation: Split into A=[0..l-1], C=[l..r], B=[r+1..], then split C at L-k into and , and merge as A + + + B. Each split/merge is O(log n) expected.
Build Complexity
Explanation: Inserting n items one-by-one costs O(n log n) expected, while constructing a Cartesian tree from indices with given priorities builds the treap in linear time.
Complexity Analysis
Code Examples
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct Node { 5 int val; // value stored at this position 6 uint32_t pr; // random priority 7 int sz; // subtree size 8 Node *l, *r; 9 Node(int v, uint32_t p) : val(v), pr(p), sz(1), l(nullptr), r(nullptr) {} 10 }; 11 12 static mt19937 rng((uint32_t)chrono::high_resolution_clock::now().time_since_epoch().count()); 13 14 int getsz(Node* t) { return t ? t->sz : 0; } 15 16 void pull(Node* t) { 17 if (!t) return; 18 t->sz = 1 + getsz(t->l) + getsz(t->r); 19 } 20 21 // Split by size: put first k elements into a, the rest into b 22 void split(Node* t, Node*& a, Node*& b, int k) { 23 if (!t) { a = b = nullptr; return; } 24 int leftsz = getsz(t->l); 25 if (k <= leftsz) { 26 // Entire split is in the left subtree 27 split(t->l, a, t->l, k); 28 b = t; 29 pull(b); 30 } else { 31 // Take current node and some from the right subtree 32 split(t->r, t->r, b, k - leftsz - 1); 33 a = t; 34 pull(a); 35 } 36 } 37 38 Node* merge(Node* a, Node* b) { 39 if (!a) return b; 40 if (!b) return a; 41 if (a->pr < b->pr) { 42 a->r = merge(a->r, b); 43 pull(a); 44 return a; 45 } else { 46 b->l = merge(a, b->l); 47 pull(b); 48 return b; 49 } 50 } 51 52 void insertAt(Node*& root, int pos, int val) { 53 Node *a, *b; 54 split(root, a, b, pos); // [0..pos-1], [pos..] 55 Node* m = new Node(val, rng()); 56 root = merge(merge(a, m), b); 57 } 58 59 void eraseAt(Node*& root, int pos) { 60 Node *a, *b, *c; 61 split(root, a, b, pos); // a=[0..pos-1], b=[pos..] 62 split(b, b, c, 1); // b=[pos], c=[pos+1..] 63 // delete b; // In contest code we may skip explicit deletion 64 root = merge(a, c); 65 } 66 67 // Access the k-th (0-based) element's value 68 int kth(Node* root, int k) { 69 Node* t = root; 70 while (t) { 71 int leftsz = getsz(t->l); 72 if (k < leftsz) t = t->l; 73 else if (k == leftsz) return t->val; 74 else { k -= leftsz + 1; t = t->r; } 75 } 76 throw runtime_error("Index out of bounds"); 77 } 78 79 void inorder(Node* t, vector<int>& out) { 80 if (!t) return; 81 inorder(t->l, out); 82 out.push_back(t->val); 83 inorder(t->r, out); 84 } 85 86 int main() { 87 ios::sync_with_stdio(false); 88 cin.tie(nullptr); 89 90 Node* root = nullptr; 91 vector<int> init = {10, 20, 30}; 92 for (int i = 0; i < (int)init.size(); ++i) insertAt(root, i, init[i]); 93 94 vector<int> v; 95 inorder(root, v); 96 cout << "Initial: "; 97 for (int x : v) cout << x << ' '; 98 cout << '\n'; 99 100 insertAt(root, 1, 99); // [10, 99, 20, 30] 101 v.clear(); inorder(root, v); 102 cout << "After insert at pos 1: "; 103 for (int x : v) cout << x << ' '; 104 cout << '\n'; 105 106 eraseAt(root, 2); // remove element at index 2 -> [10, 99, 30] 107 v.clear(); inorder(root, v); 108 cout << "After erase at pos 2: "; 109 for (int x : v) cout << x << ' '; 110 cout << '\n'; 111 112 cout << "kth(root, 1) = " << kth(root, 1) << '\n'; 113 return 0; 114 } 115
This program implements the essential implicit treap with split by size and merge. It supports insertion and deletion at arbitrary positions and retrieving the k-th element. The inorder traversal yields the current sequence. Random priorities keep the tree balanced in expectation.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct Node { 5 int val; 6 uint32_t pr; 7 int sz; 8 long long sum; // subtree sum 9 bool rev; // lazy reverse flag 10 Node *l, *r; 11 Node(int v, uint32_t p) : val(v), pr(p), sz(1), sum(v), rev(false), l(nullptr), r(nullptr) {} 12 }; 13 14 static mt19937 rng((uint32_t)chrono::high_resolution_clock::now().time_since_epoch().count()); 15 16 int getsz(Node* t) { return t ? t->sz : 0; } 17 long long getsum(Node* t) { return t ? t->sum : 0LL; } 18 19 void push(Node* t) { 20 if (!t || !t->rev) return; 21 // Apply reverse lazily: swap children and toggle children's flags 22 swap(t->l, t->r); 23 if (t->l) t->l->rev = !t->l->rev; 24 if (t->r) t->r->rev = !t->r->rev; 25 t->rev = false; 26 } 27 28 void pull(Node* t) { 29 if (!t) return; 30 t->sz = 1 + getsz(t->l) + getsz(t->r); 31 t->sum = (long long)t->val + getsum(t->l) + getsum(t->r); 32 } 33 34 void split(Node* t, Node*& a, Node*& b, int k) { 35 if (!t) { a = b = nullptr; return; } 36 push(t); 37 int leftsz = getsz(t->l); 38 if (k <= leftsz) { 39 split(t->l, a, t->l, k); 40 b = t; 41 pull(b); 42 } else { 43 split(t->r, t->r, b, k - leftsz - 1); 44 a = t; 45 pull(a); 46 } 47 } 48 49 Node* merge(Node* a, Node* b) { 50 if (!a) return b; 51 if (!b) return a; 52 if (a->pr < b->pr) { 53 push(a); 54 a->r = merge(a->r, b); 55 pull(a); 56 return a; 57 } else { 58 push(b); 59 b->l = merge(a, b->l); 60 pull(b); 61 return b; 62 } 63 } 64 65 void insertAt(Node*& root, int pos, int val) { 66 Node *a, *b; 67 split(root, a, b, pos); 68 Node* m = new Node(val, rng()); 69 root = merge(merge(a, m), b); 70 } 71 72 void rangeReverse(Node*& root, int l, int r) { // reverse inclusive range [l, r] 73 if (l > r) return; 74 Node *A, *BC, *B, *C; 75 split(root, A, BC, l); // A=[0..l-1], BC=[l..] 76 split(BC, B, C, r - l + 1); // B=[l..r], C=[r+1..] 77 if (B) B->rev = !B->rev; // toggle reverse lazily 78 root = merge(merge(A, B), C); 79 } 80 81 long long rangeSum(Node*& root, int l, int r) { 82 Node *A, *BC, *B, *C; 83 split(root, A, BC, l); 84 split(BC, B, C, r - l + 1); 85 long long ans = getsum(B); 86 root = merge(merge(A, B), C); // restore 87 return ans; 88 } 89 90 void inorder(Node* t, vector<int>& out) { 91 if (!t) return; 92 push(t); 93 inorder(t->l, out); 94 out.push_back(t->val); 95 inorder(t->r, out); 96 } 97 98 int main() { 99 ios::sync_with_stdio(false); 100 cin.tie(nullptr); 101 102 Node* root = nullptr; 103 // Build [1..10] 104 for (int i = 0; i < 10; ++i) insertAt(root, i, i + 1); 105 106 vector<int> v; 107 inorder(root, v); 108 cout << "Initial: "; 109 for (int x : v) cout << x << ' '; 110 cout << '\n'; 111 112 rangeReverse(root, 2, 7); // reverse positions 2..7 (0-based) 113 v.clear(); inorder(root, v); 114 cout << "After reverse [2..7]: "; 115 for (int x : v) cout << x << ' '; 116 cout << '\n'; 117 118 long long s = rangeSum(root, 3, 8); 119 cout << "Sum [3..8] = " << s << '\n'; 120 return 0; 121 } 122
This version adds a reverse lazy flag and a subtree sum. rangeReverse isolates [l..r] via two splits, toggles the reverse flag on the middle treap, and merges back. rangeSum isolates and reads the precomputed sum. push and pull ensure correctness of order and aggregates.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct Node { 5 int val; uint32_t pr; int sz; bool rev; Node *l, *r; 6 Node(int v, uint32_t p): val(v), pr(p), sz(1), rev(false), l(nullptr), r(nullptr) {} 7 }; 8 9 static mt19937 rng((uint32_t)chrono::high_resolution_clock::now().time_since_epoch().count()); 10 11 int getsz(Node* t){ return t? t->sz : 0; } 12 void push(Node* t){ if(!t || !t->rev) return; swap(t->l,t->r); if(t->l) t->l->rev^=1; if(t->r) t->r->rev^=1; t->rev=false; } 13 void pull(Node* t){ if(!t) return; t->sz = 1 + getsz(t->l) + getsz(t->r); } 14 15 void split(Node* t, Node*& a, Node*& b, int k){ 16 if(!t){ a=b=nullptr; return; } 17 push(t); 18 int L = getsz(t->l); 19 if(k <= L){ split(t->l, a, t->l, k); b=t; pull(b); } 20 else { split(t->r, t->r, b, k-L-1); a=t; pull(a); } 21 } 22 23 Node* merge(Node* a, Node* b){ 24 if(!a) return b; if(!b) return a; 25 if(a->pr < b->pr){ push(a); a->r = merge(a->r,b); pull(a); return a; } 26 else { push(b); b->l = merge(a,b->l); pull(b); return b; } 27 } 28 29 void insertAt(Node*& root,int pos,int val){ Node *A,*B; split(root,A,B,pos); root = merge(merge(A,new Node(val,rng())),B); } 30 31 void rotateRight(Node*& root, int l, int r, int k){ 32 if(l>r) return; Node *A,*BC,*B,*C; split(root,A,BC,l); split(BC,B,C,r-l+1); 33 int len = getsz(B); if(len<=1){ root = merge(merge(A,B),C); return; } 34 k = ((k % len) + len) % len; if(k==0){ root = merge(merge(A,B),C); return; } 35 Node *H,*T; // split B into head and tail: head has len-k elements 36 split(B,H,T,len-k); // B = [H | T], rotate right by k => [T | H] 37 B = merge(T,H); 38 root = merge(merge(A,B),C); 39 } 40 41 void inorder(Node* t, vector<int>& out){ if(!t) return; push(t); inorder(t->l,out); out.push_back(t->val); inorder(t->r,out); } 42 43 int main(){ 44 ios::sync_with_stdio(false); cin.tie(nullptr); 45 Node* root = nullptr; 46 for(int i=0;i<8;++i) insertAt(root,i,i+1); // [1..8] 47 vector<int> v; inorder(root,v); 48 cout << "Initial: "; for(int x:v) cout<<x<<' '; cout<<'\n'; v.clear(); 49 50 rotateRight(root,2,6,2); // rotate [2..6] by 2 to the right 51 inorder(root,v); 52 cout << "After rotateRight [2..6] by 2: "; for(int x:v) cout<<x<<' '; cout<<'\n'; 53 return 0; 54 } 55
The rotateRight function performs a cyclic shift on a subarray [l..r] by k positions to the right. It splits the treap into three parts, splits the middle part into head and tail at len - k, merges in swapped order, then reattaches the outer parts. Lazy reverse support is included but unused here.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct Node { 5 char ch; uint32_t pr; int sz; Node *l, *r; 6 Node(char c, uint32_t p): ch(c), pr(p), sz(1), l(nullptr), r(nullptr) {} 7 }; 8 9 static mt19937 rng((uint32_t)chrono::high_resolution_clock::now().time_since_epoch().count()); 10 11 int getsz(Node* t){ return t? t->sz:0; } 12 void pull(Node* t){ if(!t) return; t->sz = 1 + getsz(t->l) + getsz(t->r); } 13 14 void split(Node* t, Node*& a, Node*& b, int k){ 15 if(!t){ a=b=nullptr; return; } 16 int L = getsz(t->l); 17 if(k <= L){ split(t->l, a, t->l, k); b=t; pull(b); } 18 else { split(t->r, t->r, b, k-L-1); a=t; pull(a); } 19 } 20 21 Node* merge(Node* a, Node* b){ 22 if(!a) return b; if(!b) return a; 23 if(a->pr < b->pr){ a->r = merge(a->r,b); pull(a); return a; } 24 else { b->l = merge(a,b->l); pull(b); return b; } 25 } 26 27 Node* buildFromString(const string& s){ 28 Node* root = nullptr; 29 for (int i=0;i<(int)s.size();++i){ 30 Node *A,*B; split(root,A,B,i); 31 root = merge(merge(A,new Node(s[i],rng())),B); 32 } 33 return root; 34 } 35 36 string toString(Node* t){ 37 string out; out.reserve(getsz(t)); 38 // iterative inorder to avoid recursion limits (simple stack) 39 vector<Node*> st; Node* cur = t; 40 while(cur || !st.empty()){ 41 while(cur){ st.push_back(cur); cur = cur->l; } 42 cur = st.back(); st.pop_back(); 43 out.push_back(cur->ch); 44 cur = cur->r; 45 } 46 return out; 47 } 48 49 // Cut substring [l..r] and return it as a separate treap; original loses that substring 50 Node* cut(Node*& root, int l, int r){ 51 Node *A,*BC,*B,*C; split(root,A,BC,l); split(BC,B,C,r-l+1); root = merge(A,C); return B; 52 } 53 54 // Paste treap mid at position pos into root 55 void paste(Node*& root, Node* mid, int pos){ Node *A,*B; split(root,A,B,pos); root = merge(merge(A,mid),B); } 56 57 int main(){ 58 ios::sync_with_stdio(false); cin.tie(nullptr); 59 Node* A = buildFromString("hello "); 60 Node* B = buildFromString("world!"); 61 62 Node* AB = merge(A,B); // concatenate 63 cout << toString(AB) << '\n'; 64 65 // Move substring [0..4] ("hello") to the end 66 Node* mid = cut(AB, 0, 4); 67 paste(AB, mid, getsz(AB)); 68 cout << toString(AB) << '\n'; 69 70 // Extract substring [0..4] as a new string (" world") 71 Node* sub = cut(AB, 0, 5); 72 cout << toString(sub) << '\n'; 73 cout << toString(AB) << '\n'; 74 return 0; 75 } 76
This example shows rope-like operations for strings: build, concatenate (merge), cut a substring (two splits and a merge), and paste at a position (two merges). The inorder traversal reconstructs the current text.