Loading content...
The segment tree structure we built works for any associative operation. This is its true power—learn the pattern once, apply it to countless problems.
In this page, we'll implement segment trees for four common operations:
For each, only two things change: the combine function and the identity element. The tree structure, query algorithm, and update algorithm remain identical.
By the end of this page, you'll be able to implement segment trees for any associative operation. You'll understand identity elements, see complete implementations for four major operations, and learn to create generic, reusable segment tree templates.
Every segment tree operation follows this pattern:
Parameters to customize:
(a, b) => result — How to merge two valuese such that combine(e, x) = x for all xThe identity element handles edge cases:
| Operation | Combine | Identity | Why Identity Works |
|---|---|---|---|
| Sum | a + b | 0 | 0 + x = x |
| Min | min(a, b) | +∞ | min(∞, x) = x |
| Max | max(a, b) | -∞ | max(-∞, x) = x |
| GCD | gcd(a, b) | 0 | gcd(0, x) = x |
| Product | a × b | 1 | 1 × x = x |
| XOR | a ⊕ b | 0 | 0 ⊕ x = x |
| AND | a & b | all 1s | ~0 & x = x |
| OR | a | b | 0 | 0 | x = x |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
/** * Generic Segment Tree - Works for ANY associative operation */class GenericSegmentTree<T> { private tree: T[]; private n: number; private combine: (a: T, b: T) => T; private identity: T; constructor( arr: T[], combine: (a: T, b: T) => T, identity: T ) { this.n = arr.length; this.combine = combine; this.identity = identity; this.tree = new Array(4 * this.n).fill(identity); this.build(arr, 1, 0, this.n - 1); } private build(arr: T[], node: number, start: number, end: number): void { if (start === end) { this.tree[node] = arr[start]; } else { const mid = Math.floor((start + end) / 2); this.build(arr, 2 * node, start, mid); this.build(arr, 2 * node + 1, mid + 1, end); this.tree[node] = this.combine(this.tree[2 * node], this.tree[2 * node + 1]); } } query(L: number, R: number): T { return this.queryHelper(1, 0, this.n - 1, L, R); } private queryHelper(node: number, start: number, end: number, L: number, R: number): T { if (R < start || L > end) return this.identity; if (L <= start && end <= R) return this.tree[node]; const mid = Math.floor((start + end) / 2); return this.combine( this.queryHelper(2 * node, start, mid, L, R), this.queryHelper(2 * node + 1, mid + 1, end, L, R) ); } update(index: number, value: T): void { this.updateHelper(1, 0, this.n - 1, index, value); } private updateHelper(node: number, start: number, end: number, index: number, value: T): void { if (start === end) { this.tree[node] = value; } else { const mid = Math.floor((start + end) / 2); if (index <= mid) this.updateHelper(2 * node, start, mid, index, value); else this.updateHelper(2 * node + 1, mid + 1, end, index, value); this.tree[node] = this.combine(this.tree[2 * node], this.tree[2 * node + 1]); } }} // Usage examples:const arr = [3, 1, 4, 1, 5, 9, 2, 6]; const sumTree = new GenericSegmentTree(arr, (a, b) => a + b, 0);const minTree = new GenericSegmentTree(arr, (a, b) => Math.min(a, b), Infinity);const maxTree = new GenericSegmentTree(arr, (a, b) => Math.max(a, b), -Infinity);With this generic template, you never rewrite segment tree logic. Just provide combine and identity. This is software engineering best practice: abstract the invariant structure, parameterize the variant behavior.
Range sum is the most common segment tree application. We've used it throughout, but let's formalize:
Operation: Sum of all elements A[L] + A[L+1] + ... + A[R]
Combine: (a, b) => a + b
Identity: 0 (adding zero doesn't change sum)
Properties of Sum:
The invertibility is why prefix sums work for static data—but segment trees handle dynamic data better.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
class RangeSumTree { private tree: number[]; private n: number; constructor(arr: number[]) { this.n = arr.length; this.tree = new Array(4 * this.n).fill(0); this.build(arr, 1, 0, this.n - 1); } private build(arr: number[], node: number, start: number, end: number): void { if (start === end) { this.tree[node] = arr[start]; } else { const mid = (start + end) >> 1; this.build(arr, 2 * node, start, mid); this.build(arr, 2 * node + 1, mid + 1, end); this.tree[node] = this.tree[2 * node] + this.tree[2 * node + 1]; // SUM } } query(L: number, R: number): number { return this.queryHelper(1, 0, this.n - 1, L, R); } private queryHelper(node: number, start: number, end: number, L: number, R: number): number { if (R < start || L > end) return 0; // IDENTITY for sum if (L <= start && end <= R) return this.tree[node]; const mid = (start + end) >> 1; return this.queryHelper(2 * node, start, mid, L, R) + this.queryHelper(2 * node + 1, mid + 1, end, L, R); } update(index: number, value: number): void { this.updateHelper(1, 0, this.n - 1, index, value); } private updateHelper(node: number, start: number, end: number, idx: number, val: number): void { if (start === end) { this.tree[node] = val; } else { const mid = (start + end) >> 1; if (idx <= mid) this.updateHelper(2 * node, start, mid, idx, val); else this.updateHelper(2 * node + 1, mid + 1, end, idx, val); this.tree[node] = this.tree[2 * node] + this.tree[2 * node + 1]; } }} // Example:const st = new RangeSumTree([1, 3, 5, 7, 9, 11]);console.log(st.query(1, 4)); // 3+5+7+9 = 24st.update(2, 10); // Change 5 to 10console.log(st.query(1, 4)); // 3+10+7+9 = 29Range Minimum Query finds the smallest element in a range. It's used in:
Operation: min(A[L], A[L+1], ..., A[R])
Combine: (a, b) => Math.min(a, b)
Identity: +Infinity (min with infinity gives the other value)
Key Property - Idempotence: min(a, a) = a. This means overlapping ranges don't cause problems. Sparse tables exploit this for O(1) static queries—but for dynamic data, segment trees are needed.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
class RangeMinTree { private tree: number[]; private n: number; constructor(arr: number[]) { this.n = arr.length; this.tree = new Array(4 * this.n).fill(Infinity); this.build(arr, 1, 0, this.n - 1); } private build(arr: number[], node: number, start: number, end: number): void { if (start === end) { this.tree[node] = arr[start]; } else { const mid = (start + end) >> 1; this.build(arr, 2 * node, start, mid); this.build(arr, 2 * node + 1, mid + 1, end); this.tree[node] = Math.min(this.tree[2 * node], this.tree[2 * node + 1]); // MIN } } query(L: number, R: number): number { return this.queryHelper(1, 0, this.n - 1, L, R); } private queryHelper(node: number, start: number, end: number, L: number, R: number): number { if (R < start || L > end) return Infinity; // IDENTITY for min if (L <= start && end <= R) return this.tree[node]; const mid = (start + end) >> 1; return Math.min( this.queryHelper(2 * node, start, mid, L, R), this.queryHelper(2 * node + 1, mid + 1, end, L, R) ); } update(index: number, value: number): void { this.updateHelper(1, 0, this.n - 1, index, value); } private updateHelper(node: number, start: number, end: number, idx: number, val: number): void { if (start === end) { this.tree[node] = val; } else { const mid = (start + end) >> 1; if (idx <= mid) this.updateHelper(2 * node, start, mid, idx, val); else this.updateHelper(2 * node + 1, mid + 1, end, idx, val); this.tree[node] = Math.min(this.tree[2 * node], this.tree[2 * node + 1]); } }} // Example:const rmq = new RangeMinTree([5, 2, 8, 1, 9, 3]);console.log(rmq.query(0, 5)); // 1 (minimum of entire array)console.log(rmq.query(2, 4)); // 1 (min of [8, 1, 9])rmq.update(3, 10); // Change 1 to 10console.log(rmq.query(2, 4)); // 8 (min of [8, 10, 9])Often you need the INDEX of the minimum, not just the value. Store pairs (value, index) in the tree. Combine by comparing values: keep the pair with smaller value. Query returns both the min value and its position.
Range Maximum Query is the mirror of RMQ:
Operation: max(A[L], A[L+1], ..., A[R])
Combine: (a, b) => Math.max(a, b)
Identity: -Infinity
Applications include:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
class RangeMaxTree { private tree: number[]; private n: number; constructor(arr: number[]) { this.n = arr.length; this.tree = new Array(4 * this.n).fill(-Infinity); this.build(arr, 1, 0, this.n - 1); } private build(arr: number[], node: number, start: number, end: number): void { if (start === end) { this.tree[node] = arr[start]; } else { const mid = (start + end) >> 1; this.build(arr, 2 * node, start, mid); this.build(arr, 2 * node + 1, mid + 1, end); this.tree[node] = Math.max(this.tree[2 * node], this.tree[2 * node + 1]); // MAX } } query(L: number, R: number): number { return this.queryHelper(1, 0, this.n - 1, L, R); } private queryHelper(node: number, start: number, end: number, L: number, R: number): number { if (R < start || L > end) return -Infinity; // IDENTITY for max if (L <= start && end <= R) return this.tree[node]; const mid = (start + end) >> 1; return Math.max( this.queryHelper(2 * node, start, mid, L, R), this.queryHelper(2 * node + 1, mid + 1, end, L, R) ); } update(index: number, value: number): void { this.updateHelper(1, 0, this.n - 1, index, value); } private updateHelper(node: number, start: number, end: number, idx: number, val: number): void { if (start === end) { this.tree[node] = val; } else { const mid = (start + end) >> 1; if (idx <= mid) this.updateHelper(2 * node, start, mid, idx, val); else this.updateHelper(2 * node + 1, mid + 1, end, idx, val); this.tree[node] = Math.max(this.tree[2 * node], this.tree[2 * node + 1]); } }} // Example:const maxTree = new RangeMaxTree([5, 2, 8, 1, 9, 3]);console.log(maxTree.query(0, 5)); // 9 (maximum of entire array)console.log(maxTree.query(0, 2)); // 8 (max of [5, 2, 8])Range GCD finds the greatest common divisor of all elements in a range. It's useful in number theory problems and cryptographic applications.
Operation: gcd(A[L], A[L+1], ..., A[R])
Combine: (a, b) => gcd(a, b)
Identity: 0 (gcd(0, x) = x)
Why GCD Works:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
// Euclidean GCD algorithmfunction gcd(a: number, b: number): number { a = Math.abs(a); b = Math.abs(b); while (b !== 0) { const temp = b; b = a % b; a = temp; } return a;} class RangeGCDTree { private tree: number[]; private n: number; constructor(arr: number[]) { this.n = arr.length; this.tree = new Array(4 * this.n).fill(0); this.build(arr, 1, 0, this.n - 1); } private build(arr: number[], node: number, start: number, end: number): void { if (start === end) { this.tree[node] = arr[start]; } else { const mid = (start + end) >> 1; this.build(arr, 2 * node, start, mid); this.build(arr, 2 * node + 1, mid + 1, end); this.tree[node] = gcd(this.tree[2 * node], this.tree[2 * node + 1]); // GCD } } query(L: number, R: number): number { return this.queryHelper(1, 0, this.n - 1, L, R); } private queryHelper(node: number, start: number, end: number, L: number, R: number): number { if (R < start || L > end) return 0; // IDENTITY for GCD if (L <= start && end <= R) return this.tree[node]; const mid = (start + end) >> 1; return gcd( this.queryHelper(2 * node, start, mid, L, R), this.queryHelper(2 * node + 1, mid + 1, end, L, R) ); } update(index: number, value: number): void { this.updateHelper(1, 0, this.n - 1, index, value); } private updateHelper(node: number, start: number, end: number, idx: number, val: number): void { if (start === end) { this.tree[node] = val; } else { const mid = (start + end) >> 1; if (idx <= mid) this.updateHelper(2 * node, start, mid, idx, val); else this.updateHelper(2 * node + 1, mid + 1, end, idx, val); this.tree[node] = gcd(this.tree[2 * node], this.tree[2 * node + 1]); } }} // Example:const gcdTree = new RangeGCDTree([12, 18, 24, 36, 48]);console.log(gcdTree.query(0, 4)); // 6 (GCD of all)console.log(gcdTree.query(1, 3)); // 6 (GCD of 18, 24, 36)gcdTree.update(2, 15); // Change 24 to 15console.log(gcdTree.query(0, 4)); // 3 (GCD of 12, 18, 15, 36, 48)| Operation | Combine | Identity | Idempotent | Common Use Cases |
|---|---|---|---|---|
| Sum | a + b | 0 | No | Totals, counts, averages |
| Min | min(a,b) | +∞ | Yes | Lowest price, RMQ, LCA |
| Max | max(a,b) | -∞ | Yes | Highest score, peak finding |
| GCD | gcd(a,b) | 0 | Yes | Number theory, coprimality |
| LCM | lcm(a,b) | 1 | Yes | Synchronization problems |
| AND | a & b | ~0 | Yes | Bit intersection queries |
| OR | a | b | 0 | Yes | Bit union queries |
| XOR | a ^ b | 0 | No | Parity, toggle state |
| Product | a × b | 1 | No | Factorial ranges, probabilities |
Most competitive programming problems use Sum, Min, or Max. GCD appears in number theory contexts. XOR is useful for parity and duplicate-finding problems. If your operation is associative and has an identity element, segment trees will work.
What's Next in Chapter 27:
This module covered the foundational segment tree structure. The chapter continues with:
You now have the conceptual foundation. Practice implementing these trees from scratch until the pattern becomes second nature.
Congratulations! You've mastered the segment tree concept—from understanding why simpler approaches fail, through the divide-and-conquer structure, to implementing various query types. This knowledge is essential for any serious algorithm practitioner.