Segment Tree
A binary tree over array intervals that answers range queries (sum, min, max, gcd) and point or range updates in O(log n).
Definition
A segment tree stores an array a[0..n-1] as a binary tree where each node covers a contiguous interval and holds an aggregate (sum, min, max, gcd, …) of that interval. The root covers [0, n-1], its children cover the two halves, and leaves cover single elements. Any query interval [l, r] decomposes into at most 2 log₂ n node intervals, so a range query is O(log n), and a point update only touches the O(log n) ancestors of one leaf.
The aggregate can be any associative operation with an identity (a monoid): + with 0, min with +∞, max with -∞, gcd with 0, matrix product, or custom structs like "maximum subarray sum". With lazy propagation the tree also supports range updates (add v to every element in [l, r]) in O(log n) by deferring work to nodes until they are visited.
Compared to a Fenwick Tree, a segment tree is more general (any monoid, range updates, "first index where prefix ≥ x" descents) at the cost of 2–4× memory and more code. Compared to a Sparse Table, it supports updates.
Intuition
A mental model before the formal terms.
Think of a tournament bracket over the array where each match records the "winner" (max) or the "combined score" (sum) of the two teams below. To find the max over a range, you do not look at every element — you look at the few bracket nodes whose spans tile exactly your range. Updating one element re-plays only the matches on its path to the final, about log n of them.
A range [l, r] in an array of 16 elements can always be covered by at most 8 bracket nodes, and usually far fewer; that is the whole trick.
How it works
- Build: node
icovers[lo, hi]; iflo == historea[lo]; otherwise build children on[lo, mid]and[mid+1, hi]and combine. With 1-based indexing children ofiare2iand2i+1; allocate4nslots. - Query(l, r) at node covering
[lo, hi]: if[lo, hi]is disjoint from[l, r]return the identity; if fully inside return the node value; otherwise combine the results of both children. - Point update(idx, v): descend to the leaf for
idx, set it, and recompute each ancestor on the way back up. - Range update with lazy propagation: when an update fully covers a node, apply it to the node's aggregate and record the pending change in
lazy[i]; before descending into a node, push its pending change to its children. Queries push down the same way. - An iterative bottom-up version (size padded to a power of two, leaves at
n..2n-1) is shorter and faster for point-update/range-query.
Why it works
Any interval [l, r] is split by the recursion into maximal aligned nodes: at each depth at most two nodes are partially covered (the ones containing l and r), so at most 2 nodes per level are visited and at most 4 log n nodes total.
Associativity guarantees that combining aggregates of adjacent pieces in tree order equals the aggregate over the whole range, regardless of how the range is split.
Lazy propagation is correct because a pending tag on a node exactly represents the effect not yet applied to its subtree; pushing it before any descent keeps every visited node accurate.
Operations
| Operation | Description | Cost |
|---|---|---|
| build(a) | Construct the tree bottom-up from the array. | O(n) |
| query(l, r) | Combine O(log n) covering nodes. | O(log n) |
| update(i, v) | Set a leaf and recompute its ancestors. | O(log n) |
| rangeUpdate(l, r, v) | Apply to covering nodes, deferring to children via lazy tags. | O(log n) |
| descend / find first | Walk down to the first index where a prefix aggregate crosses a threshold. | O(log n) |
Recognition
How to tell a problem wants this.
- An array with
qinterleaved range queries and updates,n, q ≤ 2·10^5—O(nq)is too slow. - Query words: sum/min/max/gcd/count "in the range
[l, r]", or "after updating index i". - Range assignment or range increment together with range query — lazy propagation.
- Counting inversions, "number of elements less than x so far", or offline sweeps over coordinates.
Interactive demo
Play, step, change the input. ← → and space work too.
1build(node, l, r): if l == r: tree[node] = a[l]2 else: build children over [l,mid], [mid+1,r]; tree[node] = left + right3query(node, l, r, ql, qr):4 if qr < l or r < ql: return 0 # no overlap5 if ql <= l and r <= qr: return tree[node] # total overlap6 return query(left) + query(right) # partial overlap: split7update(node, l, r, i, v): descend to leaf i, set it, recompute sums on the way upPseudocode
1build(node, lo, hi):2 if lo == hi: tree[node] = a[lo]; return3 mid = (lo + hi) / 24 build(2node, lo, mid); build(2node+1, mid+1, hi)5 tree[node] = combine(tree[2node], tree[2node+1])6query(node, lo, hi, l, r):7 if r < lo or hi < l: return IDENTITY8 if l <= lo and hi <= r: return tree[node]9 return combine(query(left half), query(right half))10update(node, lo, hi, idx, v):11 if lo == hi: tree[node] = v; return12 recurse into the half containing idx; tree[node] = combine(children)Implementation
11 · Storage: 4n tree array, combine and identity2class SegmentTree:3 """Range-sum segment tree with point update. Swap combine/identity for min, max, gcd."""4 5 identity = 06 7 def __init__(self, a: list[int]):8 self.n = len(a)9 self.tree = [0] * (4 * self.n if self.n else 4)10 if self.n:11 self._build(a, 1, 0, self.n - 1)12 13 @staticmethod14 def combine(x: int, y: int) -> int:15 return x + y16 172 · Build: leaves store a[i], parents combine children18 def _build(self, a: list[int], node: int, lo: int, hi: int) -> None:19 if lo == hi:20 self.tree[node] = a[lo]21 return22 mid = (lo + hi) // 223 self._build(a, 2 * node, lo, mid)24 self._build(a, 2 * node + 1, mid + 1, hi)25 self.tree[node] = self.combine(self.tree[2 * node], self.tree[2 * node + 1])26 273 · Range query: prune, take, or split28 def query(self, l: int, r: int) -> int:29 """Aggregate over the closed range [l, r]."""30 return self._query(1, 0, self.n - 1, l, r)31 32 def _query(self, node: int, lo: int, hi: int, l: int, r: int) -> int:33 if r < lo or hi < l: # disjoint34 return self.identity35 if l <= lo and hi <= r: # fully covered36 return self.tree[node]37 mid = (lo + hi) // 238 return self.combine(39 self._query(2 * node, lo, mid, l, r),40 self._query(2 * node + 1, mid + 1, hi, l, r),41 )42 434 · Point update: descend to leaf, recombine ancestors44 def update(self, idx: int, value: int) -> None:45 """Set a[idx] = value."""46 self._update(1, 0, self.n - 1, idx, value)47 48 def _update(self, node: int, lo: int, hi: int, idx: int, value: int) -> None:49 if lo == hi:50 self.tree[node] = value51 return52 mid = (lo + hi) // 253 if idx <= mid:54 self._update(2 * node, lo, mid, idx, value)55 else:56 self._update(2 * node + 1, mid + 1, hi, idx, value)57 self.tree[node] = self.combine(self.tree[2 * node], self.tree[2 * node + 1])self.treeis a flat list of4nzeros with 1-based indexing; children ofnodeare2 * nodeand2 * node + 1.combineis a@staticmethodandidentitya class attribute, so a subclass overrides both to switch the aggregate (e.g.minwithfloat("inf"))._buildrecurses to leaves (lo == hi), storesa[lo], and combines the halves bottom-up in O(n)._queryprunes disjoint nodes (returnsidentity), takes fully covered nodes, and combines both children for partial overlap._updatefollows the single root-to-leaf path foridx, then recombines every ancestor.
Recursion depth is only O(log n) (~20 levels for n = 10^6), so Python's 1000-frame limit is never a concern — unlike O(n)-deep plain-BST recursions. Function-call overhead still makes this several times slower than the iterative version.
- Python ints are arbitrary precision, so there is no overflow concern for sums.
(lo + hi) // 2is floor division; plain/would produce a float index.- The recursive helper pattern (
querypublic wrapper,_querywith node bounds) keeps the API clean.
- Allocating
2 * nslots for the recursive layout instead of4 * n. - Overriding
combinetominbut forgetting to overrideidentity(must becomefloat("inf")). - Calling
query(l, r)with half-open semantics; this implementation is closed-inclusive on both ends.
- Overflow: C++ needs
long longfor large sums; JS/TS numbers silently lose integer precision past 2^53 (useBigInt); Python ints are arbitrary precision. - Genericity: the TS version injects the monoid through constructor generics; C++ would use a template parameter; Python and JS swap
combine/identityby subclassing. - Identity for min/max:
LLONG_MAX/LLONG_MINin C++,Infinity/-Infinityin JS/TS,float("inf")in Python — forgetting to change it is the classic porting bug. - Recursion depth is O(log n) everywhere, so even Python's 1000-frame default limit is safe — but Python call overhead makes its iterative variant noticeably faster.
Complexity
| Operation | Average | Worst | Note |
|---|---|---|---|
| Access | O(log n) | O(log n) | Single element = query(i, i). |
| Search | O(n) | O(n) | O(log n) tree descent for monotone aggregates (e.g. first prefix ≥ x). |
| Insert | — | — | Fixed size; rebuild in O(n). |
| Delete | — | — | Set to identity in O(log n). |
| Update | O(log n) | O(log n) | |
| Build | O(n) | O(n) | |
| Range query | O(log n) | O(log n) | |
| Point update | O(log n) | O(log n) | |
| Range update (lazy) | O(log n) | O(log n) | |
| Space | O(n) | 4n slots with recursive 1-based layout, 2n with the iterative layout; lazy adds another array. | |
Advantages & disadvantages
- Handles any associative operation, not just invertible ones like sum.
- Range updates via lazy propagation;
O(log n)for every operation. - Supports advanced tricks: tree walks, persistent versions, merging, 2D variants.
- Memory
4n(or2niterative) versusnfor a Fenwick tree. - Considerably more code; lazy propagation is error-prone under time pressure.
- Constant factors are higher than a Fenwick tree for plain prefix sums.
Use cases
- Range minimum/maximum/sum queries with point updates (Range Sum Query – Mutable).
- Range add + range sum, range assign + range min (lazy).
- Counting inversions or elements-less-than-x by indexing values instead of positions.
- Sweep-line geometry (area of union of rectangles), scheduling with capacity constraints.
- Range queries interleaved with updates on an array of size up to about
10^6. - The aggregate is not invertible (min, max, gcd) so prefix sums cannot be subtracted.
- Range updates are required — lazy propagation.
- "First index where the running aggregate crosses a threshold" via tree descent.
- No updates — a Prefix Sum array or Sparse Table is simpler and faster.
- Only prefix sums with point updates — a Fenwick Tree uses less memory and less code.
- Queries are few (
q · nis small) — brute force.
Alternatives
Common mistakes
- Allocating
2nslots with the recursive 1-based layout — it needs4nwhennis not a power of two. - Returning
0instead of the true identity for min/max queries (should be+∞/-∞). - Forgetting to push lazy tags before descending in a query, returning stale child values.
- Multiplying a lazy add by the wrong interval length, or applying the tag to the node but not to
lazy[node]. - Off-by-one between closed
[l, r]and half-open[l, r)conventions.
Interview patterns
- Range Sum Query – Mutable: build,
update,sumRange. - Count of smaller numbers after self / inversions: coordinate-compress values, iterate from the right, query prefix count, then point-add.
- Range add + range sum with lazy propagation.
- Skyline / rectangle union area via sweep line with a "count and covered length" segment tree.
- Merge-sort tree or persistent segment tree for kth smallest in a range (advanced).
- Recognizing the approach from an array and a targetIntermediate
- Deciding whether O(n²) can be improvedIntermediate
- Where does O(n log n) come from?Beginner
- Average case versus worst caseIntermediate
- Minimum Size Subarray SumIntermediate
- Kth Largest Element in an ArrayIntermediate
- Subarray Sum Equals KIntermediate