Segment Tree Patterns — Comprehensive Guide
A segment tree answers range queries and applies updates on an array in O(log N) each. The core idea: recursively split the array into halves, store an aggregate (sum, min, max, gcd, …) for every segment, and reuse those aggregates instead of rescanning. Once you see it as a mergeable-aggregate over a recursive split, everything from range-sum to range-assign-with-lazy becomes one skeleton with a different combine function.
Quick Navigation: “I need to…”
| I need to… | Technique | Section |
|---|---|---|
| Query range sum/min/max with point updates | Basic segment tree | 2 |
| Write it short and fast | Iterative segment tree | 3 |
| Swap sum for min/max/gcd without rewriting | The merge abstraction | 4 |
| Apply an update to a whole range | Lazy propagation | 5 |
| Range assign (set all to x) + range query | Lazy with assignment | 5 |
| Find first index with prefix condition | Descent on the tree | 6 |
| Count / query on values not indices | Segment tree over value domain | 7 |
| Handle values too large to index | Coordinate compression | 7 |
| Query historical versions | Persistent segment tree | 8 |
| Just count inversions / order-stats | BIT (Fenwick) alternative | 9 |
| See full worked LeetCode solutions | 307 / 699 / 327 walkthroughs | 9.5 |
Table of Contents
- Why a Segment Tree
- Basic Segment Tree (Recursive)
- Iterative Segment Tree
- The Merge Abstraction
- Lazy Propagation (Range Updates)
- Binary Search on the Tree
- Segment Tree on Values
- Persistent Segment Tree
- When to Use a BIT Instead 9.5. Worked LeetCode Problems
- Common Patterns Collection
- Pattern Recognition Cheat Sheet
1. Why a Segment Tree
The Problem It Solves
You have an array and a mix of two operations, interleaved:
- Update: change element(s).
- Query: aggregate over a range
[l, r](sum, min, max, gcd, …).
| Approach | Update | Range query |
|---|---|---|
| Plain array | O(1) | O(N) |
| Prefix sums | O(N) (rebuild) | O(1) |
| Segment tree | O(log N) | O(log N) |
Prefix sums are unbeatable when there are no updates. The moment updates interleave with queries, prefix sums die (every update forces an O(N) rebuild), and the segment tree wins.
The Structure
Each node covers a contiguous range. Root covers [0, n-1]. A node covering [l, r] with l != r splits at mid = (l+r)/2 into [l, mid] and [mid+1, r]. Leaves cover single elements.
array = [5, 3, 7, 9, 6, 2] (sum tree)
[0,5]=32
/ \
[0,2]=15 [3,5]=17
/ \ / \
[0,1]=8 [2,2]=7 [3,4]=15 [5,5]=2
/ \ / \
[0,0]=5 [1,1]=3 [3,3]=9 [4,4]=6
A range query decomposes [l, r] into O(log N) node ranges that exactly tile it. That’s the whole
trick — and the reason only O(log N) of them are ever needed is derived in
The Three Cases in Query below.
Master LeetCode Comparison Table
The five representative segment-tree problems, ordered by the sub-pattern they force. Read the “Tree indexed by” column first — it is the single fork that decides everything else.
| LC # | Problem | Difficulty | Tree indexed by | Node stores | Update kind | Query answers | Sub-pattern (§) |
|---|---|---|---|---|---|---|---|
| 307 | Range Sum Query - Mutable | Medium | array position | sum | point assign | range sum | Basic point-update (§2/§3) |
| 699 | Falling Squares | Hard | compressed x | max height | range assign-max (lazy) | range max | Lazy propagation (§5) |
| 715 | Range Module | Hard | compressed x | covered? | range assign 0/1 (lazy) | range all-covered? | Lazy assign (§5) |
| 327 | Count of Range Sums | Hard | prefix value | count | point +1 | count in value range | Tree on values (§7) |
| 1649 | Create Sorted Array | Hard | element value | count | point +1 | count smaller / larger | Tree on values (§7) |
★ Insight ─────────────────────────────────────
Two problems (307, 699) index by position — the leaf is array slot i. Two (327, 1649) index by value — the leaf is “how many elements equal value v seen so far”, and the array position is thrown away. That axis choice is the whole design decision: position-indexed answers “aggregate over a slice”; value-indexed answers “how many seen so far are smaller/larger/in-range”. Same tree, opposite meaning of the index. The counting (value-indexed) rows can all be done with a shorter BIT instead — reach for the segment tree only when you also need min/max/assign or range-updates.
─────────────────────────────────────────────────
2. Basic Segment Tree (Recursive)
Point update, range query. The clearest version to learn first.
class SegmentTree:
def __init__(self, data):
self.n = len(data)
self.tree = [0] * (4 * self.n) # 4N is a safe upper bound
if self.n:
self._build(data, 1, 0, self.n - 1)
def _build(self, data, node, lo, hi):
if lo == hi:
self.tree[node] = data[lo]
return
mid = (lo + hi) // 2
self._build(data, 2 * node, lo, mid)
self._build(data, 2 * node + 1, mid + 1, hi)
self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]
def update(self, i, val):
self._update(1, 0, self.n - 1, i, val)
def _update(self, node, lo, hi, i, val):
if lo == hi:
self.tree[node] = val # set index i to val
return
mid = (lo + hi) // 2
if i <= mid:
self._update(2 * node, lo, mid, i, val)
else:
self._update(2 * node + 1, mid + 1, hi, i, val)
self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]
def query(self, l, r):
return self._query(1, 0, self.n - 1, l, r)
def _query(self, node, lo, hi, l, r):
if r < lo or hi < l: # node range fully outside query
return 0 # identity for sum
if l <= lo and hi <= r: # node range fully inside query
return self.tree[node]
mid = (lo + hi) // 2
return (self._query(2 * node, lo, mid, l, r) +
self._query(2 * node + 1, mid + 1, hi, l, r))
The Three Cases in Query
Every recursive query node hits exactly one of these:
query [l, r] vs node [lo, hi]:
Case A: no overlap -> return identity (0 for sum)
[lo...hi] [l...r]
Case B: total overlap -> return stored aggregate (stop, don't recurse)
[l ... lo...hi ... r]
Case C: partial overlap -> recurse into both children, combine
[lo ... [l...] ... hi]
Why this visits only O(log N) nodes
Case C is the only one that spawns more work — A and B return immediately. So the cost of a query is just how many Case C nodes exist, and the bound comes from a counting argument on each level of the tree.
Claim: at most two nodes per level are Case C. The nodes on any one level partition the array
into consecutive, non-overlapping blocks. A node is Case C only when the query boundary l or r
falls strictly inside its block. Since l lies inside exactly one block on that level, and r
lies inside exactly one, at most two blocks per level can be partially overlapped. Every other
block on the level is either entirely inside [l, r] (Case B, stops) or entirely outside (Case A,
stops).
With at most 2 Case C nodes per level, each spawning 2 children, we touch at most 4 nodes per
level. The tree has log N levels, so a query visits O(log N) nodes total — not O(N), even though
the query range itself may cover most of the array.
This is also the precise reason Case B must not recurse. Recursion is what costs; stopping at a fully-covered node is what collapses an arbitrarily wide range into a handful of stored aggregates. If you “simplify” the code by dropping the total-overlap check and descending to the leaves, every node in the range becomes Case C and the query silently degrades to O(N) — correct answers, useless performance.
Why 4 * n?
The recursion tree is not perfectly balanced when n is not a power of two, and with heap indexing
(node, 2*node, 2*node+1) an unbalanced tree leaves gaps in the index space. Make it concrete
with n = 5, splitting each range at its midpoint:
node 1 [0,4]
/ \
node 2 [0,2] node 3 [3,4]
/ \ / \
node 4 [0,1] node 5 [2,2] node 6 [3,3] node 7 [4,4]
/ \
node 8 node 9
[0,0] [1,1]
Only 9 nodes are actually used, but the largest index reached is 9, already past 2n = 10’s
comfortable margin — and a less even split pushes it higher. The worst case is bounded by
2 * next_power_of_two(n), which for n = 5 is 2 * 8 = 16. Since next_power_of_two(n) < 2n,
the bound 4 * n always covers it, which is why 4 * n is the standard allocation.
If you round n up to a power of two before building, the tree is perfect, no gaps appear, and
2 * n suffices.
Complexity
| Op | Time |
|---|---|
| Build | O(N) |
| Point update | O(log N) |
| Range query | O(log N) |
| Space | O(N) |
3. Iterative Segment Tree
The bottom-up iterative form is shorter, ~2x faster (no recursion overhead), and the standard in competitive programming. It stores leaves at tree[n .. 2n-1] and internal nodes at tree[1 .. n-1].
class SegTreeIterative:
def __init__(self, data):
self.n = len(data)
self.tree = [0] * (2 * self.n)
# place leaves
for i in range(self.n):
self.tree[self.n + i] = data[i]
# build internal nodes bottom-up
for i in range(self.n - 1, 0, -1):
self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]
def update(self, i, val):
i += self.n
self.tree[i] = val
i //= 2
while i >= 1:
self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]
i //= 2
def query(self, l, r):
"""Sum over [l, r] inclusive."""
res = 0
l += self.n
r += self.n + 1 # half-open [l, r+1)
while l < r:
if l & 1: # l is a right child -> include, move right
res += self.tree[l]
l += 1
if r & 1: # r is a right child -> its left sibling is in range
r -= 1
res += self.tree[r]
l //= 2
r //= 2
return res
The Climb
Query [2, 4] on n=6, leaves at indices 6..11:
l = 6+2 = 8, r = 6+4+1 = 11
l=8 even, r=11 odd -> r=10, res += tree[10]; l=4, r=5
l=4 even, r=5 odd -> r=4, res += tree[4]; l=2, r=2
l == r -> stop
The two odd-index checks are the whole algorithm: an odd index means “you are a right child, so your parent would over-cover the range — grab yourself and step inward.”
Trade-offs vs Recursive
| Recursive | Iterative | |
|---|---|---|
| Speed | Slower (call overhead) | ~2x faster |
| Readability | Higher | Lower (index tricks) |
| Lazy propagation | Natural | Awkward (do lazy recursively) |
| Line count | More | Fewer |
Rule of thumb: iterative for point-update + range-query hot loops; recursive when you need lazy propagation.
4. The Merge Abstraction
Sum, min, max, gcd — all the same tree, differing only in the combine function and its identity. Factor those two out and one class handles every variant.
class SegTree:
def __init__(self, data, combine, identity):
self.n = len(data)
self.combine = combine # e.g. min, max, lambda a,b: a+b, math.gcd
self.identity = identity # neutral element for combine
self.tree = [identity] * (2 * self.n)
for i in range(self.n):
self.tree[self.n + i] = data[i]
for i in range(self.n - 1, 0, -1):
self.tree[i] = combine(self.tree[2 * i], self.tree[2 * i + 1])
def update(self, i, val):
i += self.n
self.tree[i] = val
i >>= 1
while i:
self.tree[i] = self.combine(self.tree[2 * i], self.tree[2 * i + 1])
i >>= 1
def query(self, l, r):
resl = resr = self.identity
l += self.n
r += self.n + 1
while l < r:
if l & 1:
resl = self.combine(resl, self.tree[l]); l += 1
if r & 1:
r -= 1; resr = self.combine(self.tree[r], resr)
l >>= 1; r >>= 1
return self.combine(resl, resr)
Identity Elements
| Operation | combine | identity |
|---|---|---|
| Sum | a + b |
0 |
| Min | min(a, b) |
+inf |
| Max | max(a, b) |
-inf |
| GCD | math.gcd(a, b) |
0 |
| Product | a * b |
1 |
| Bitwise AND | a & b |
~0 (all ones) |
| Bitwise OR | a \| b |
0 |
Why Two Accumulators (resl, resr)?
For non-commutative merges (matrix product, string concatenation, “max subarray” structs) order matters. Left-side nodes must combine left-to-right and right-side nodes right-to-left, then join. For sum/min/max it doesn’t matter, but keeping both accumulators makes the template correct for all cases.
★ Insight ─────────────────────────────────────
The requirement is that combine be associative. It need not be commutative. Sum/min/max/gcd are both. But “merge two max-subarray structs” (holding total, best-prefix, best-suffix, best-answer) is associative-only — and the two-accumulator query above handles it correctly where a single accumulator would not.
─────────────────────────────────────────────────
5. Lazy Propagation (Range Updates)
Point update is O(log N). But a range update (“add 5 to every element in [l, r]”) touched naively is O(N log N). Lazy propagation defers the work: mark a node “this whole subtree owes an update” and only push it down when a query actually descends into that subtree. Range update becomes O(log N).
The Two Lazy Flavors
- Range add:
a[i] += xfor alliin[l, r]. Lazy value accumulates additively. - Range assign:
a[i] = xfor alliin[l, r]. Lazy value overwrites; needs a “has pending assign” marker.
Range Add + Range Sum
class LazySegTree:
def __init__(self, data):
self.n = len(data)
self.tree = [0] * (4 * self.n)
self.lazy = [0] * (4 * self.n) # pending "+x" for this node's whole range
if self.n:
self._build(data, 1, 0, self.n - 1)
def _build(self, data, node, lo, hi):
if lo == hi:
self.tree[node] = data[lo]
return
mid = (lo + hi) // 2
self._build(data, 2 * node, lo, mid)
self._build(data, 2 * node + 1, mid + 1, hi)
self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]
def _apply(self, node, lo, hi, val):
"""Apply a pending +val to this whole node range."""
self.tree[node] += val * (hi - lo + 1) # sum grows by val per element
self.lazy[node] += val # remember to push to children
def _push_down(self, node, lo, hi):
if self.lazy[node]:
mid = (lo + hi) // 2
self._apply(2 * node, lo, mid, self.lazy[node])
self._apply(2 * node + 1, mid + 1, hi, self.lazy[node])
self.lazy[node] = 0
def update(self, l, r, val):
self._update(1, 0, self.n - 1, l, r, val)
def _update(self, node, lo, hi, l, r, val):
if r < lo or hi < l:
return
if l <= lo and hi <= r:
self._apply(node, lo, hi, val) # whole node covered: mark lazy, stop
return
self._push_down(node, lo, hi)
mid = (lo + hi) // 2
self._update(2 * node, lo, mid, l, r, val)
self._update(2 * node + 1, mid + 1, hi, l, r, val)
self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]
def query(self, l, r):
return self._query(1, 0, self.n - 1, l, r)
def _query(self, node, lo, hi, l, r):
if r < lo or hi < l:
return 0
if l <= lo and hi <= r:
return self.tree[node]
self._push_down(node, lo, hi)
mid = (lo + hi) // 2
return (self._query(2 * node, lo, mid, l, r) +
self._query(2 * node + 1, mid + 1, hi, l, r))
The Three Lazy Primitives
Every lazy segment tree is these three functions. Get them right, the rest is boilerplate:
| Function | Job |
|---|---|
_apply(node, lo, hi, val) |
Apply an update to a node’s aggregate and record it in lazy |
_push_down(node) |
Move this node’s pending lazy to both children, then clear it |
pull-up (tree[node] = combine(children)) |
Recompute aggregate after children change |
Order matters: always _push_down before recursing into children on a partial overlap, and pull-up after.
Range Assign Variant
Assignment overwrites rather than accumulates, so you need a sentinel meaning “no pending assign” (assign of 0 is a real value, so a separate flag or None is required).
Note — this is a patch on top of
LazySegTreeabove, not standalone code. It replaces_apply/_push_downand additionally requires aself.has_lazy = [False] * (4 * self.n)array added in__init__(the assign-marker). Pasting these two methods alone willAttributeErroronself.has_lazy.
def _apply_assign(self, node, lo, hi, val):
self.tree[node] = val * (hi - lo + 1)
self.lazy[node] = val
self.has_lazy[node] = True
def _push_down(self, node, lo, hi):
if self.has_lazy[node]:
mid = (lo + hi) // 2
self._apply_assign(2 * node, lo, mid, self.lazy[node])
self._apply_assign(2 * node + 1, mid + 1, hi, self.lazy[node])
self.has_lazy[node] = False
Combining Add + Assign
If a problem mixes both (assign then add), an assign must clear any pending add on that node, and a later add stacks on top. Keep two lazy fields plus an assign flag; when applying assign, reset the add lazy to the added amount. This is fiddly — draw the state machine before coding.
6. Binary Search on the Tree
“Find the first index i where prefix-sum reaches k” or “leftmost position with value >= x”. Instead of querying prefixes in O(log^2 N), descend the tree in a single O(log N) walk, deciding left-vs-right at each node.
Find First Prefix Sum >= k
def find_first_ge(self, k):
"""Smallest index i such that sum(a[0..i]) >= k. Assumes non-negative values."""
if self.tree[1] < k:
return -1 # total sum too small
node, lo, hi = 1, 0, self.n - 1
while lo < hi:
mid = (lo + hi) // 2
if self.tree[2 * node] >= k: # answer is in left subtree
node, hi = 2 * node, mid
else:
k -= self.tree[2 * node] # consume left, go right
node, lo = 2 * node + 1, mid + 1
return lo
Why This Beats Binary-Search-Over-Query
The naive way binary searches the index and calls query(0, mid) each step: O(log N) steps × O(log N) per query = O(log^2 N). The descent fuses them into one O(log N) walk because the tree already stores the prefix aggregates you’d otherwise recompute.
Classic use: the CSES-style “list removal / k-th remaining element” problem — build a tree of 1s, and find_first_ge(k) finds the k-th surviving position, then set that leaf to 0.
7. Segment Tree on Values
Build the tree indexed by value (or value-rank), not array position. Leaf v holds “how many elements equal v” (or a sum, etc.). Range query then answers value-range questions: count of elements in [a, b], k-th smallest, number of inversions.
Count Inversions
Use a BIT for this in practice. Counting inversions needs only prefix sums with point increments, which is exactly a Fenwick tree’s job — half the code and a smaller constant. See Fenwick Tree §9, which is canonical for the counting/order-statistics family (LC 315, 493, CSES Inversions). The version below is here for one reason: if you already have a segment tree in the file for another part of the problem, reusing it beats writing a second structure.
Which tree / assign vs add: this uses
SegTreeIterativefrom Section 3, whoseupdate(i, val)assigns (overwrites) leafi. Because it has no “+=” operation, we emulate an increment by reading the current count and assigningcount + 1(st.update(r, st.query(r, r) + 1)). If you swap in a tree whoseupdateadds a delta, drop the read and just dost.update(r, 1).
def count_inversions(arr):
# compress values to ranks 0..m-1
ranks = {v: i for i, v in enumerate(sorted(set(arr)))}
m = len(ranks)
st = SegTreeIterative([0] * m) # counts per value; update() ASSIGNS, not adds
inversions = 0
for x in reversed(arr):
r = ranks[x]
inversions += st.query(0, r - 1) if r > 0 else 0 # already-seen smaller values
st.update(r, st.query(r, r) + 1) # assign count+1 (emulated increment)
return inversions
Coordinate Compression
Values can be up to 1e9 but there are only N of them. Map them to 0..N-1 by sorting the distinct values — then a tree of size N indexes them. This is the standard companion to value-indexed segment trees (and BITs).
def compress(arr):
order = sorted(set(arr))
rank = {v: i for i, v in enumerate(order)}
return [rank[v] for v in arr], order # order[i] recovers original value
★ Insight ─────────────────────────────────────
Inversions, “count smaller after self”, k-th order statistic, and “range count of values” are all the same value-indexed tree with a different query. Recognizing the value-domain reframing is the actual skill; the tree is just plumbing. A BIT (Section 9) does the counting versions with less code.
─────────────────────────────────────────────────
8. Persistent Segment Tree
A persistent tree keeps every past version accessible. Each update creates O(log N) new nodes (the path from root to leaf) and shares the rest with the previous version. N updates cost O(N log N) space total.
The Idea
Update index i: copy only the root-to-leaf path.
Every off-path child pointer is reused from the old version.
version k version k+1
root_k root_{k+1} (new)
/ \ / \
L R --> L'(new) R (R shared!)
... ...
Node-Pointer Implementation
class PersistentSegTree:
def __init__(self, n):
self.n = n
self.left = [0] # child pointers, index 0 = null node
self.right = [0]
self.val = [0]
def _new(self, l, r, v):
self.left.append(l); self.right.append(r); self.val.append(v)
return len(self.val) - 1
def build(self, lo, hi):
if lo == hi:
return self._new(0, 0, 0)
mid = (lo + hi) // 2
l = self.build(lo, mid)
r = self.build(mid + 1, hi)
return self._new(l, r, self.val[l] + self.val[r])
def update(self, prev, lo, hi, i, delta):
"""Return root of a NEW version with a[i] += delta, sharing prev's untouched nodes."""
if lo == hi:
return self._new(0, 0, self.val[prev] + delta)
mid = (lo + hi) // 2
if i <= mid:
l = self.update(self.left[prev], lo, mid, i, delta)
r = self.right[prev] # shared
else:
l = self.left[prev] # shared
r = self.update(self.right[prev], mid + 1, hi, i, delta)
return self._new(l, r, self.val[l] + self.val[r])
def query(self, node, lo, hi, l, r):
if r < lo or hi < l or node == 0:
return 0
if l <= lo and hi <= r:
return self.val[node]
mid = (lo + hi) // 2
return (self.query(self.left[node], lo, mid, l, r) +
self.query(self.right[node], mid + 1, hi, l, r))
Killer Application: K-th Smallest in a Range
Store a prefix of versions (version i = value-counts of a[0..i-1]). To answer “k-th smallest in a[l..r]”, walk two versions simultaneously (root[r+1] minus root[l]) and descend by count — O(log N) per query, no updates needed. This is the standard offline range-kth-order-statistic solution.
9. When to Use a BIT Instead
A Binary Indexed Tree (Fenwick) does prefix-sum + point-update in O(log N) with far less code. If your problem only needs those two, prefer it. Full treatment (range-update BIT, 2D BIT, k-th descent): Fenwick Tree Patterns.
class BIT:
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1) # 1-indexed
def update(self, i, delta): # a[i] += delta (0-indexed i)
i += 1
while i <= self.n:
self.tree[i] += delta
i += i & (-i) # add lowest set bit
def prefix(self, i): # sum of a[0..i] (0-indexed i)
i += 1
s = 0
while i > 0:
s += self.tree[i]
i -= i & (-i) # strip lowest set bit
return s
def range_sum(self, l, r):
return self.prefix(r) - (self.prefix(l - 1) if l > 0 else 0)
Segment Tree vs BIT
| BIT (Fenwick) | Segment Tree | |
|---|---|---|
| Operations | Prefix-sum-style (invertible: sum, xor) | Any associative merge (min, max, gcd, …) |
| Range min/max | ✗ (not invertible) | ✓ |
| Range update + range query | Harder (two BITs trick) | Natural (lazy) |
| Code size | Tiny | Larger |
| Constant factor | Smaller | Larger |
| Persistence | Awkward | Natural |
Decision: sum/xor + point updates → BIT. Min/max/gcd, range assign, or “first index where…” → segment tree.
9.5 Worked LeetCode Problems
Three problems, one per core sub-pattern from the Master Comparison Table: a position-indexed point-update tree (307), a lazy range-assign tree (699), and a value-indexed count tree (327). Every solution below was executed against the LeetCode canonical answers before being written here.
Problem 307 — Range Sum Query - Mutable
Difficulty: Medium
Statement. Given an integer array nums, support two operations any number of times, interleaved: update(index, val) sets nums[index] = val, and sumRange(left, right) returns the sum of nums[left..right] inclusive.
This is the “hello world” of segment trees — and the exact problem FenwickTree §9.5 also solves, which is the whole lesson: when the aggregate is just a sum, a BIT is shorter. The segment tree earns its extra code only when you need min/max/assign. We use the iterative tree from Section 3 because point-update + range-sum is exactly its sweet spot.
class NumArray:
def __init__(self, nums):
self.n = len(nums)
self.tree = [0] * (2 * self.n)
for i, v in enumerate(nums): # leaves live at [n, 2n)
self.tree[self.n + i] = v
for i in range(self.n - 1, 0, -1): # build internal nodes bottom-up
self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]
def update(self, index, val):
i = index + self.n
self.tree[i] = val # ASSIGN the leaf (not += a delta)
i //= 2
while i >= 1: # re-pull every ancestor
self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]
i //= 2
def sumRange(self, left, right):
res = 0
l, r = left + self.n, right + self.n + 1 # half-open [l, r)
while l < r:
if l & 1: res += self.tree[l]; l += 1
if r & 1: r -= 1; res += self.tree[r]
l //= 2; r //= 2
return res
Trace on nums = [1, 3, 5] (leaves at indices 3,4,5; internal 1,2):
build: tree[3..5] = 1,3,5
tree[2] = tree[4]+tree[5] = 3+5 = 8
tree[1] = tree[2]+tree[3] = 8+1 = 9
sumRange(0,2): l=3, r=6
l=3 odd -> res=1, l=4 (leaf 0 grabbed directly)
l=4 even, r=6 even
l=2, r=3
l=2 even, r=3 odd -> r=2, res += tree[2]=8 -> res=9
l=1, r=1 stop => 9 ✓
update(1, 2): leaf 4 = 2; tree[2]=2+5=7; tree[1]=7+1=8
sumRange(0,2) now => 8 ✓
★ Insight ─────────────────────────────────────
The point-update here assigns (self.tree[i] = val) — unlike a BIT, whose update adds a delta and therefore needs delta = val - old[index] plus a shadow copy of the array. The segment-tree leaf holds the absolute value, so it can overwrite in place with no shadow array. That is a small but real ergonomic win of the iterative segment tree over a BIT for the “set element = value” flavor of update.
─────────────────────────────────────────────────
Problem 699 — Falling Squares
Difficulty: Hard
Statement. Squares drop one at a time onto a number line. Square i is given as positions[i] = [left, sideLength], occupying the interval [left, left + sideLength - 1] horizontally. A falling square lands on top of the tallest surface currently under any part of its footprint (the ground is height 0), so its new top height is (max height under footprint) + sideLength. After each drop, report the tallest stack seen so far. Return the list of running maxima.
This is the canonical LeetCode teacher for lazy range-assign-max. Coordinates go up to 1e8, so we coordinate-compress the endpoints first (Section 7’s companion technique), then run a recursive lazy tree whose merge is max.
def fallingSquares(positions):
xs = set() # compress interval endpoints
for l, s in positions:
xs.add(l); xs.add(l + s - 1)
order = sorted(xs)
idx = {v: i for i, v in enumerate(order)}
n = len(order)
tree = [0] * (4 * n)
lazy = [0] * (4 * n) # pending "height is at least this"
def apply(node, val):
tree[node] = max(tree[node], val) # pull-up merge AND lazy-merge are both max
lazy[node] = max(lazy[node], val)
def push(node):
if lazy[node]:
apply(2 * node, lazy[node]); apply(2 * node + 1, lazy[node])
lazy[node] = 0
def update(node, lo, hi, l, r, val):
if r < lo or hi < l: return
if l <= lo and hi <= r:
apply(node, val); return # whole node covered: assign-max, stop
push(node); mid = (lo + hi) // 2
update(2 * node, lo, mid, l, r, val)
update(2 * node + 1, mid + 1, hi, l, r, val)
tree[node] = max(tree[2 * node], tree[2 * node + 1])
def query(node, lo, hi, l, r):
if r < lo or hi < l: return 0
if l <= lo and hi <= r: return tree[node]
push(node); mid = (lo + hi) // 2
return max(query(2 * node, lo, mid, l, r),
query(2 * node + 1, mid + 1, hi, l, r))
res = []; best = 0
for l, s in positions:
a, b = idx[l], idx[l + s - 1]
newh = query(1, 0, n - 1, a, b) + s # land on tallest under footprint
update(1, 0, n - 1, a, b, newh) # raise the whole footprint to newh
best = max(best, newh)
res.append(best)
return res
Trace on positions = [[1,2],[2,3],[6,1]]:
endpoints: sq0 [1,2], sq1 [2,4], sq2 [6,6] -> compressed {1,2,4,6}
sq0 [1,2]: query footprint = 0, newh = 0+2 = 2, assign-max 2 over [1,2]. best=2
sq1 [2,4]: query footprint (covers x=2, held by sq0 at h=2) = 2,
newh = 2+3 = 5, assign-max 5 over [2,4]. best=5
sq2 [6,6]: query footprint = 0 (empty column), newh = 0+1 = 1. best=5
=> [2, 5, 5] ✓
★ Insight ─────────────────────────────────────
Assign-max is the easy lazy flavor: max is idempotent, so a leftover stale lazy re-applied to a node changes nothing, and 0 is a safe “no pending” sentinel — no has_lazy flag needed (contrast the assign-sum variant in §5, where lazy 0 is ambiguous with “assign the value 0”). The problem shape “each new item sits on the current max under its span, then raises that span” — stacking boxes, booking the tallest hotel floor in a range — is the fingerprint that says lazy assign-max.
─────────────────────────────────────────────────
Problem 327 — Count of Range Sums
Difficulty: Hard
Statement. Given an integer array nums and two integers lower and upper, return the number of range sums S(i, j) = nums[i] + ... + nums[j] (with i <= j) that lie in [lower, upper] inclusive.
The reframe is the whole solution. With prefix sums P[0..n], a range sum S(i,j) = P[j+1] - P[i]. Counting lower <= P[j+1] - P[i] <= upper over all i <= j becomes: sweep the prefixes left to right, and for each new prefix p, count how many already-seen prefixes P[i] satisfy p - upper <= P[i] <= p - lower. That is a value-range count over a value-indexed tree — Section 7’s pattern, done here with the shorter BIT counting form.
from bisect import bisect_left, bisect_right
def countRangeSum(nums, lower, upper):
prefix = [0]
for x in nums:
prefix.append(prefix[-1] + x)
order = sorted(set(prefix)) # compress prefix VALUES
rank = {v: i for i, v in enumerate(order)}
m = len(order)
tree = [0] * (m + 1) # value-indexed count BIT (1-indexed)
def upd(i):
i += 1
while i <= m: tree[i] += 1; i += i & (-i)
def qpre(i): # count inserted with rank in [0, i]
i += 1; s = 0
while i > 0: s += tree[i]; i -= i & (-i)
return s
def qrange(loval, hival): # count inserted prefix values in [loval, hival]
li = bisect_left(order, loval) # first rank whose value >= loval
ri = bisect_right(order, hival) - 1 # last rank whose value <= hival
if li > ri: return 0
return qpre(ri) - (qpre(li - 1) if li > 0 else 0)
count = 0
for p in prefix:
count += qrange(p - upper, p - lower) # earlier prefixes making a valid range END here
upd(rank[p]) # then register p as a candidate start
return count
Trace on nums = [-2, 5, -1], lower = -2, upper = 2:
prefix = [0, -2, 3, 2]
p=0: need earlier P[i] in [0-2, 0+2]=[-2,2] -> none inserted yet -> 0. insert 0
p=-2: need P[i] in [-4, 0] -> {0} qualifies -> +1. insert -2
p=3: need P[i] in [1, 5] -> {0,-2}? none in [1,5] -> 0. insert 3
p=2: need P[i] in [0, 4] -> {0, 3} qualify (0 and 3) -> +2. insert 2
total = 3 ✓ (ranges [0,0]=-2, [2,2]=-1, [0,2]=2)
★ Insight ─────────────────────────────────────
The counting axis is the prefix value, not the array index — the tree never knows where in the array a prefix came from, only its magnitude. The i <= j (earlier-index) constraint is enforced purely by when you insert: query for the answer before inserting the current prefix, so only strictly-earlier prefixes are in the tree. That “query-then-insert” ordering is the same trick behind counting inversions and “count smaller after self” — the sweep direction encodes the index constraint for free. And because we only count (never min/max), a BIT replaces the segment tree with less code.
─────────────────────────────────────────────────
10. Common Patterns Collection
Range Sum with Point Updates (CSES “Dynamic Range Sum Queries”)
Basic tree (Section 2) or BIT (Section 9). Read updates and ? queries, dispatch.
Range Minimum with Point Updates (CSES “Dynamic Range Minimum Queries”)
Same skeleton, combine = min, identity = +inf (Section 4).
Range Add + Range Sum (CSES “Range Update Queries”)
Lazy propagation, range-add flavor (Section 5).
Range Assign + Range Sum
Lazy propagation, assign flavor with a has_lazy flag (Section 5).
K-th Element / Order Statistics
Value-indexed tree with count leaves + find_first_ge descent (Sections 6, 7).
Count of Smaller Numbers After Self / Inversions
Value-indexed tree or BIT, sweep right-to-left (Section 7).
Longest / Max-Subarray in Range
Store a struct per node (total, best_prefix, best_suffix, best); merge is associative but not commutative — use the two-accumulator query (Section 4).
def merge(a, b):
total = a.total + b.total
best_prefix = max(a.best_prefix, a.total + b.best_prefix)
best_suffix = max(b.best_suffix, b.total + a.best_suffix)
best = max(a.best, b.best, a.best_suffix + b.best_prefix)
return Node(total, best_prefix, best_suffix, best)
11. Pattern Recognition Cheat Sheet
By Problem Signal
| You see… | Reach for | Section |
|---|---|---|
| “Range sum, updates in between” | Basic tree / BIT | 2, 9 |
| “Range min / max / gcd + updates” | Merge abstraction | 4 |
| “Add x to all in [l, r]” | Lazy (range add) | 5 |
| “Set all in [l, r] to x” | Lazy (range assign) | 5 |
| “First index where prefix >= k” | Tree descent | 6 |
| “K-th smallest / count in value range” | Value-indexed tree | 7 |
| “Count inversions / smaller-after-self” | Value tree or BIT | 7, 9 |
| “K-th smallest in a range, many queries” | Persistent tree | 8 |
| “Values up to 1e9, only N distinct” | Coordinate compression first | 7 |
The Universal Skeleton
Every segment tree is:
build: leaf = data; internal = combine(left, right)
update: change leaf (or mark lazy); pull up ancestors
query: tile [l, r] with O(log N) fully-covered nodes; combine them
The only things that change between variants:
- combine function + its identity
- whether you carry lazy (range updates)
- whether the index axis is position or value
Common Pitfalls
| Pitfall | Fix |
|---|---|
| Array too small | Allocate 4 * n (recursive) or 2 * n (iterative) |
| Forgetting to push lazy before recursing | Always _push_down on partial overlap |
| Forgetting to pull up after child update | Recompute tree[node] after recursion |
| Assign-lazy of value 0 ignored | Use a separate has_lazy flag, not lazy == 0 |
| Using BIT for range min | BIT needs invertible ops; min isn’t — use segment tree |
| Non-commutative merge with one accumulator | Keep resl and resr separate |
| Off-by-one in iterative query | Query range is half-open [l, r+1) |
Complexity Summary
| Operation | Time | Space |
|---|---|---|
| Build | O(N) | O(N) |
| Point update | O(log N) | — |
| Range query | O(log N) | — |
| Range update (lazy) | O(log N) | — |
| Tree descent (find first) | O(log N) | — |
| Persistent update | O(log N) time, O(log N) new nodes | O(N log N) total |
| K-th in range (persistent) | O(log N) per query | O(N log N) |
Practice Order
Start here
│
▼
307 (Easy) ──── Range Sum Query Mutable: the basic point-update + range-sum tree (§2)
│
▼
RMQ build/query ── Range Minimum Query: same skeleton, combine=min, identity=+inf (§4)
│
▼
315 (Hard*) ──── Count of Smaller After Self: value-indexed tree, sweep right→left (§7)
│ (*Hard-tagged but a gentle intro to the value-domain reframe)
▼
Range-add lazy ──── Range Sum with range update: lazy propagation, add flavor (§5)
│
▼
732 (Hard) ──── My Calendar III: lazy range-add + coordinate compression (§5, §7)
│
▼
699 (Hard) ──── Falling Squares: lazy range-ASSIGN + compression, max query (§5, §7)
Worked in full in §9.5: 307 (point-update, §2/§3), 699 (lazy assign-max, §5), 327 (value-indexed count, §7).
When This Fails
A segment tree needs an associative merge with an identity. Beyond that, the practical limits:
- The operation is not associative. The tree combines children in a fixed shape, so
merge(merge(a,b),c)must equalmerge(a,merge(b,c)). Sum, min, max, gcd, matrix product: yes. “Average” and “median”: no, at least not directly — store enough to reconstruct them instead. - Sum with point updates only. Then a Fenwick tree does the same job in half the code with a smaller constant.
- No updates at all. A prefix sum gives O(1) queries after O(n) setup. Building a tree is strictly worse.
- Lazy tags that do not compose. Mixing range-assign with range-add requires deciding what happens when both are pending on one node. If your tag algebra is not closed under composition, the propagation will be wrong.
- Persistence without the space budget. A persistent tree costs O(log n) new nodes per update, so O(n log n) total. Check that fits before committing.
Self-Test
Answer these from memory, out loud or on paper, before looking. Recognition is not recall: rereading an explanation feels like knowing, and it is not. A question you cannot answer cold names the exact section to revisit — you do not need to reread the guide.
1. What must be true of the merge function?
Answer
It must be associative and have an identity element (for out-of-range queries). It need not be invertible — that is exactly the freedom a segment tree buys over a Fenwick tree.
2. Why is a query O(log n)? Give the actual argument.
Answer
At each of the log n levels, the query range can partially overlap at most two nodes — one at each end. Everything strictly inside is fully covered and returns immediately. So you touch O(1) nodes per level, hence O(log n) total.
3. Why allocate 4n rather than 2n?
Answer
When n is not a power of two, the recursive 1-based layout leaves gaps, and indices can reach up to just under 4n. Allocating 2n works only for the iterative bottom-up variant or when n is padded to a power of two.
4. What is lazy propagation, and what invariant does push-down maintain?
Answer
A pending update stored at a node, covering its whole range, not yet applied to its children. The invariant: a node’s stored value is always correct for its range, while its children may be stale. So before descending, push the tag down; before reading a child, ensure the parent has pushed.
5. Why does build take O(n) and not O(n log n)?
Answer
Each node is constructed exactly once, doing O(1) merge work. There are 2n − 1 nodes. There is no per-node log factor because you never search — you fill bottom-up.
6. What does “segment tree on values” mean, and when do you need it?
Answer
Index by value rank rather than array position: tree[v] counts how many times value v has been inserted. It answers “how many values less than x have I seen?” and is the shape behind inversion counting and order statistics. Compress values to ranks first.
7. How does a persistent segment tree get away with O(log n) space per version?
Answer
An update touches only the root-to-leaf path — log n nodes. Every other node is shared with the previous version by pointer. So each version costs O(log n) new nodes rather than a full copy.
8. Segment tree or Fenwick — decide.
Answer
Fenwick if the aggregate is invertible (sum, xor) and you need point updates with prefix or range queries: shorter, faster, smaller. Segment tree if you need min, max, gcd, assignment, lazy range updates, persistence, or a descend-the-tree search.
See Also
- Fenwick Tree (BIT) — shorter and faster whenever the aggregate is invertible (sum, xor) and you only need point updates.
- Prefix Sum — if nothing ever updates, do not build a tree: O(1) queries after an O(n) scan.
- Tree Patterns §2 —
tin/toutflattening turns a subtree into a contiguous range, which is what lets a segment tree answer subtree queries. - Binary Search — the descend-the-tree query in §6 is a binary search that reuses the tree’s own structure instead of wrapping it in a search loop.
- Pattern Decision Map — the router: which technique does a cold problem call for?
- Pattern Mastery Program — the spaced-repetition schedule, mastery checklist, and drill formats that turn reading into recall.
Pattern mastered — one skeleton (build = combine children, update = change leaf then pull up, query = tile the range with O(log N) covered nodes), and two questions: (1) do I need lazy, i.e. range-updates? (2) is my index axis position or value? Sum-only, no range-update, invertible → a BIT is shorter; min/max/assign/range-update → the segment tree is why you’re here.