Segment Tree Patterns & Practice
You've learned the fundamentals, building & querying, updates, and lazy propagation. Now let's apply segment trees to real problems, explore variations beyond sums, and build a mental toolkit for recognizing when to use them.
Pattern 1: Range Sum with Point Updates
This is the "hello world" of segment trees. Given an array, answer sum(l, r) queries and handle update(i, val) operations.
Classic Problems
- Range Sum Query, Mutable (LeetCode 307): Direct application.
- Count of Smaller Numbers After Self (LeetCode 315): Build segment tree over value range, process array right-to-left.
- Dynamic Range Sum Queries (CSES): Point update + range sum — the cleanest pure seg tree problem.
- Count Inversions: Same idea, segment tree on values, count elements already seen that are greater.
Template: Range Sum + Point Update
class SegTree {
int n;
vector<long long> t;
void build(vector<int>& a, int v, int s, int e) {
if (s == e) { t[v] = a[s]; return; }
int m = (s+e)/2;
build(a, 2*v, s, m); build(a, 2*v+1, m+1, e);
t[v] = t[2*v] + t[2*v+1];
}
void upd(int v, int s, int e, int i, long long val) {
if (s == e) { t[v] = val; return; }
int m = (s+e)/2;
if (i <= m) upd(2*v, s, m, i, val);
else upd(2*v+1, m+1, e, i, val);
t[v] = t[2*v] + t[2*v+1];
}
long long qry(int v, int s, int e, int l, int r) {
if (r < s || e < l) return 0;
if (l <= s && e <= r) return t[v];
int m = (s+e)/2;
return qry(2*v, s, m, l, r) + qry(2*v+1, m+1, e, l, r);
}
public:
SegTree(vector<int>& a) : n(a.size()), t(4*n) { build(a,1,0,n-1); }
void update(int i, int v) { upd(1,0,n-1,i,v); }
long long query(int l, int r) { return qry(1,0,n-1,l,r); }
};
Pattern 2: Range Min/Max Queries
Replace sum with min (or max). The only changes:
- Merge:
min(left, right)instead ofleft + right - Identity:
INT_MAXinstead of0
Classic Problems
- Dynamic Range Minimum Queries (CSES): The textbook RMQ application with point updates.
- Sliding Window Maximum (LeetCode 239): Can solve with segment tree on the array, query max of sliding window. (Deque is optimal, but seg tree works and generalizes.)
- Salary Queries (CSES): Update values, query “how many in range [a,b]?” — coordinate compress + seg tree on values.
Template: Range Min + Point Update
class MinSegTree {
int n;
vector<int> t;
void build(vector<int>& a, int v, int s, int e) {
if (s == e) { t[v] = a[s]; return; }
int m = (s+e)/2;
build(a, 2*v, s, m); build(a, 2*v+1, m+1, e);
t[v] = min(t[2*v], t[2*v+1]);
}
void upd(int v, int s, int e, int i, int val) {
if (s == e) { t[v] = val; return; }
int m = (s+e)/2;
if (i <= m) upd(2*v, s, m, i, val);
else upd(2*v+1, m+1, e, i, val);
t[v] = min(t[2*v], t[2*v+1]);
}
int qry(int v, int s, int e, int l, int r) {
if (r < s || e < l) return INT_MAX;
if (l <= s && e <= r) return t[v];
int m = (s+e)/2;
return min(qry(2*v, s, m, l, r), qry(2*v+1, m+1, e, l, r));
}
public:
MinSegTree(vector<int>& a) : n(a.size()), t(4*n) { build(a,1,0,n-1); }
void update(int i, int v) { upd(1,0,n-1,i,v); }
int query(int l, int r) { return qry(1,0,n-1,l,r); }
};
Pattern 3: Range Update + Range Query (Lazy)
When both updates and queries operate on ranges. This requires lazy propagation.
Classic Problems
- Range Updates and Sums (CSES): Add a value to a range or set a range to a value, then query range sum. The core lazy propagation problem — requires careful handling of two lazy types.
- Range Update Queries (CSES): Add a value to a range, query single elements. Simpler lazy intro.
- Circular RMQ (Codeforces 52C): Range add + range min on a circular array. Good test of lazy propagation with modular indices.
- Painting the Fence: Range color assignments, then query how many segments have a certain color.
Pattern 4: Custom Merge Functions
Sometimes the "aggregate" stored at each node is more complex than a single number. As long as two children can be merged in O(1), the segment tree works.
Examples
| Node Stores | Merge Logic | Use Case |
|---|---|---|
{sum, max, min} | Component-wise combine | Multiple stats in one pass |
{prefix_sum, suffix_sum, total, max_subarray} | Kadane-style merge | Max subarray sum with updates |
{count_zeros, count_ones} | Addition | Count occurrences with flips |
{matrix 2×2} | Matrix multiplication | Fibonacci range queries |
Example: Maximum Subarray Sum (LeetCode 53 variant with updates)
Each node stores four values:
total: sum of the rangeprefix: maximum prefix sumsuffix: maximum suffix sumbest: maximum subarray sum within the range
Merge is:
Node merge(Node left, Node right) {
return {
.total = left.total + right.total,
.prefix = max(left.prefix, left.total + right.prefix),
.suffix = max(right.suffix, right.total + left.suffix),
.best = max({left.best, right.best,
left.suffix + right.prefix})
};
}
This is one of the most elegant segment tree applications. It handles point updates in O(log n) and answers "what's the maximum subarray sum in [l, r]?" in O(log n).
Pattern 5: Coordinate Compression + Segment Tree
When values are large (up to 109) but there are few distinct values (up to 105), compress values to indices:
- Collect all values, sort and deduplicate.
- Map each value to its rank (0, 1, 2, ...).
- Build a segment tree over ranks, not original values.
Used in: counting inversions, merge sort tree, order-statistics queries.
Advanced Variations (Brief Overview)
Persistent Segment Tree
Every update creates a new "version" of the tree, reusing unchanged nodes. Gives access to any historical version. Uses O(n log n) space total for n updates. Classic application: kth smallest in any subarray.
2D Segment Tree
A segment tree of segment trees. The outer tree splits rows; each node stores an inner tree over columns. Supports 2D range queries (e.g., "sum of sub-matrix") in O(log2 n).
Segment Tree Beats
Handles operations like "clamp all elements in [l, r] to be ≤ v" (range-min assignment). Uses "break" and "tag" conditions to achieve amortized O(n log2 n) for a sequence of operations.
Merge Sort Tree
Each node stores the sorted list of elements in its range. Queries like "how many elements in [l, r] are ≤ k" can be answered in O(log2 n) using binary search at each visited node.
Pattern Recognition Cheat Sheet
| If the problem says... | Think... |
|---|---|
| "range sum/min/max with updates" | Basic segment tree |
| "add value to range" + "query range" | Segment tree with lazy propagation |
| "count elements < k in range" | Merge sort tree or persistent seg tree |
| "kth smallest in range" | Persistent segment tree |
| "maximum subarray in range with updates" | Custom merge segment tree (pattern 4) |
| "2D sub-matrix queries" | 2D segment tree or BIT |
| "offline queries on ranges" | Consider sweep + segment tree |
| "range query, no updates" | Sparse table or prefix sums (simpler!) |
Common Pitfalls in Competitive Programming
- Using segment tree when not needed: If there are no updates, prefix sums or sparse tables are simpler and faster.
- Forgetting 1-based vs 0-based: Tree is 1-indexed, but the array can be either. Convert carefully at the public interface.
- Overflow: Sum of 105 elements each up to 109 exceeds
int. Uselong long. - Wrong lazy push-down: For non-additive operations (set, min-assignment), the push-down logic differs. Think carefully about how lazy tags compose.
- Allocating 2n instead of 4n: For non-power-of-2 sizes, 2n isn't enough. Always use 4n.
Segment Tree vs. Fenwick Tree (BIT)
| Segment Tree | Fenwick Tree (BIT) | |
|---|---|---|
| Range query | O(log n) | O(log n) |
| Point update | O(log n) | O(log n) |
| Range update | O(log n) with lazy | O(log n) for add (with trick) |
| Min/Max query | ✔ | ✘ (only prefix min/max) |
| Custom merge | ✔ | ✘ |
| Code length | ~30-50 lines | ~15-20 lines |
| Memory | 4n | n |
| Constant factor | Larger | Smaller (faster in practice) |
Rule of thumb: Use Fenwick for range-sum + point-update problems (it's faster and shorter). Use segment tree for everything else (min/max, custom merges, range updates, lazy propagation).
Practice Problems
Curated problems from beginner to advanced. Each has a brief description and progressive hints — reveal them one at a time only when you’re stuck.
Beginner: Core Segment Tree
CSES — Dynamic Range Sum Queries
Given an array, handle point updates and range sum queries. The first segment tree problem everyone should solve.
Hint 1: What to store
Build a segment tree where each node stores the sum of all elements in its range. A leaf stores a single element’s value. A parent stores left_child + right_child.
Hint 2: How to update
For update(i, val): start at the root and walk down to the leaf at index i (go left if i is in the left half, right otherwise). Set the leaf to val. On the way back up, recalculate each ancestor: tree[node] = tree[left] + tree[right].
Hint 3: How to query
For query(l, r): at each node, check three cases. No overlap (node range entirely outside [l,r]) → return 0. Total overlap (node range entirely inside [l,r]) → return stored sum. Partial overlap → recurse into both children and add results.
CSES — Dynamic Range Minimum Queries
Same structure as sum queries, but query the minimum in a range. Tests that you understand how to swap the merge function.
Hint 1: What changes from sum
Only two things change. The merge function becomes min(left, right) instead of left + right. And the “no overlap” base case returns ∞ (e.g., INT_MAX) instead of 0, because min(anything, ∞) = anything.
Hint 2: Update logic
Identical to the sum version — change the leaf, then recalculate each parent as min(tree[2*v], tree[2*v+1]) instead of summing.
Hint 3: Why this generalizes
This same swap works for any associative function: max, GCD, AND, OR, XOR. The pattern: (1) change the merge, (2) change the identity element. The tree structure and recursion stay the same.
LeetCode 307 — Range Sum Query Mutable
Implement a class with update(index, val) and sumRange(left, right). Good for validating your template against LeetCode’s judge.
Hint 1: Constructor
In the constructor, build a standard sum segment tree from the input array. Allocate 4*n space for the tree array.
Hint 2: Update semantics
LeetCode’s update(index, val) sets the value (not adds). Walk to the leaf, set it to val, then recalculate ancestors. If your template uses “add delta,” compute delta = val - old_value first.
Hint 3: Watch for indexing
LeetCode uses 0-based indexing. Make sure your tree’s build(0, n-1) and query/update ranges match. Off-by-one errors are the #1 source of WA.
Query the XOR of elements in a range. Can be solved without a segment tree, but building one with XOR merge is great practice.
Hint 1: Why XOR works as a merge
XOR is associative and commutative. Its identity element is 0 (a⊕0 = a). So it plugs into a segment tree exactly like sum does: merge = XOR, identity = 0.
Hint 2: Segment tree approach
Each node stores the XOR of all elements in its range. Build, update, and query are identical to the sum template with + replaced by ^.
Hint 3: Simpler alternative (if no updates)
Without updates, a prefix XOR array works: pre[i] = a[0]^a[1]^...^a[i]. Then xor(l,r) = pre[r] ^ pre[l-1]. The seg tree is better when you also have point updates.
Intermediate: Lazy Propagation
Add a value to all elements in a range, then query individual element values. The gentlest introduction to lazy propagation.
Hint 1: Why this is easier than full lazy
Since queries are point queries (single index), you only walk root-to-leaf. You just need to accumulate lazy tags along the path. No merging children for range results.
Hint 2: Lazy approach
For range add [l,r,d]: standard lazy range update (total overlap → add to node + set lazy, partial → push down then recurse). For point query at i: walk root to leaf, pushing down lazy tags at each level. The leaf value is the answer.
Hint 3: Even simpler alternative
A Fenwick tree (BIT) with the range-update-point-query trick works: bit_add(l, d); bit_add(r+1, -d) and point query = bit_prefix_sum(i). But implementing the lazy seg tree is great practice for harder problems.
Support both “add d to range” and “set range to v” with range sum queries. Hard because of the two-tag interaction.
Hint 1: Two types of lazy tags
You need two lazy fields per node: set_val (pending assignment) and add_val (pending addition). Use a boolean has_set to tell if a set is pending (since the set value could be 0).
Hint 2: How the tags interact
New set arrives: it overwrites everything — clear any existing add tag. New add at a node with a pending set: the add modifies the set value (set_val += add_delta). Add at a node with only an add: they sum up.
Hint 3: Push-down order
Apply set first (if pending): child.value = set_val × child_range_size, clear child’s add. Then apply add: child.value += add_val × child_range_size. Always clear the parent’s tags after pushing.
Hint 4: The apply formulas
For “set to v”: tree[node] = v × range_size (every element becomes v, sum = v × count). For “add d”: tree[node] += d × range_size (each element increases by d).
Range add and range min queries on a circular array. Tests lazy propagation plus handling wrap-around indices.
Hint 1: Handling the circular part
If l ≤ r, normal range [l, r]. If l > r (wraps around), split into two operations: [l, n−1] and [0, r]. Apply both and combine results (min of both for queries, update both for modifications).
Hint 2: Lazy for min + add
When you add d to every element in a range, the minimum shifts by exactly d. So: tree[node] += d (no × range_size!), lazy[node] += d. Merge: min(left, right).
Hint 3: Push-down details
tree[child] += lazy[parent], lazy[child] += lazy[parent], lazy[parent] = 0. Identity for no-overlap: LLONG_MAX.
Codeforces 242E — XOR on Segment
Range XOR update + range sum query. You can’t directly combine XOR updates with sums — needs a clever bit decomposition trick.
Hint 1: Why direct lazy doesn’t work
XOR-ing every element by v doesn’t give a simple formula for the new sum. Example: XOR-ing [3, 5] by 1 gives [2, 4] — sum went from 8 to 6. There’s no “sum += f(v, range_size)” formula.
Hint 2: Think bit by bit
Handle each of the 20 bit positions independently. For bit k, maintain a segment tree where each node stores the count of elements that have bit k set. The total sum = Σ count[k] × 2k.
Hint 3: XOR as a flip
XOR-ing by v: for each bit k where v has a 1, flip the count: count = range_size − count. Elements that had the bit now don’t, and vice versa. Use a lazy tag (0 or 1) to track pending flips.
Hint 4: Putting it together
Build 20 segment trees (or one tree with 20-element nodes). For sum query on [l,r]: query each bit tree and compute Σ count × 2k. For XOR update with v: for each bit set in v, do a range flip on that bit tree.
Intermediate: Counting & Inversions
LeetCode 315 — Count of Smaller Numbers After Self
For each element, count how many elements to its right are strictly smaller. Introduces two important ideas: “segment tree on values” and “coordinate compression.”
Hint 1: Process right-to-left
We need elements to the right that are smaller. If we scan from right to left, every element we’ve already processed is to the right of the current one. So the question becomes: “of the values I’ve seen so far, how many are less than the current value?”
Hint 2: Segment tree on the value range
Instead of a tree on array indices, build a tree on the value range. Position v in the tree means “how many times value v has appeared so far.” To count values less than x, query sum(0, x−1). To record seeing value x, do update(x, +1).
Hint 3: What is coordinate compression and why we need it
If values can be up to 109, we can’t create an array of size 109. Coordinate compression solves this: collect all values that actually appear, sort them, and assign each a small index. Example: values [100, −5, 999, 100] → unique sorted: [−5, 100, 999] → mapped to [0, 1, 2]. Now −5 is index 0, 100 is index 1, 999 is index 2. The tree only needs size 3 instead of 109. Relative order is preserved, so “smaller than” queries still work.
Hint 4: Full algorithm
(1) Coordinate compress all values. (2) Create a sum segment tree of size = number of unique values, initialized to all zeros. (3) Traverse the array right-to-left. For element with compressed index v: answer[i] = query(0, v−1), then update(v, +1).
Count pairs where i < j and nums[i] > 2 × nums[j]. Similar to inversions but with the 2× twist.
Hint 1: Reduce to a counting problem
Process left to right. At index j, ask: “how many previously-inserted values are > 2×nums[j]?” This is a “count values in a range” query on the value-based segment tree.
Hint 2: Coordinate compress both values and doubled values
You query about 2*nums[j] but insert nums[i]. Your compressed set must include both the original values and the doubled values. Collect all nums[i] and all 2*nums[j] together, sort, deduplicate, map to 0..k−1.
Hint 3: Query and insert
For each j: let c = compressed index of 2*nums[j]. Query sum(c+1, max_index) to count values > 2×nums[j]. Then let v = compressed index of nums[j], do update(v, +1).
Employees have salaries that can change. Query: how many have salary in [a, b]? Values up to 109, so coordinate compression is needed.
Hint 1: Why and how to coordinate compress
Salaries up to 109 — too big for an array. But at most ~4×105 distinct values matter (initial + update + query values). Read all input first, collect every salary value and query bound, sort, deduplicate, map to 0..k−1. Build the tree on this compressed range.
Hint 2: What the tree stores
Position c = “how many employees currently have salary with compressed index c.” Initialize by iterating over starting salaries and doing update(compress(salary), +1) for each.
Hint 3: Updates and queries
Salary change (old → new): update(compress(old), −1) then update(compress(new), +1). Query “how many in [a,b]”: query(compress(a), compress(b)). Use lower_bound to find compressed indices for query bounds.
Given n ranges, for each count how many others it contains and how many contain it. Sweep + segment tree.
Hint 1: Sort by left endpoint
Sort by left ascending. Same left? Sort by right descending (wider range first — it might contain narrower ones with the same start).
Hint 2: “Contains” count via sweep
After sorting, range A at position i contains range B at position j > i if A.right ≥ B.right (A.left ≤ B.left is guaranteed by sort). Process in order: for each range, “how many right endpoints already in the tree are ≤ my right?” = query(0, compress(right_i)). Then update(compress(right_i), +1).
Hint 3: “Contained by” count
Reverse: process in reverse sorted order. For each range, “how many right endpoints already in the tree are ≥ my right?” = query(compress(right_i), max_index). That gives the count of ranges that contain the current one.
Hint 4: Compress the right endpoints
Right endpoints can be large, so coordinate compress them. The segment tree is on compressed right values with point updates and range sum queries.
Advanced: Custom Merge & Complex Queries
After point updates, query the maximum subarray sum of the entire array. Requires 4 fields per node.
Hint 1: Why one field isn’t enough
The maximum subarray might span the boundary between two children. Knowing each child’s best subarray isn’t enough — you need the best suffix of the left child and best prefix of the right child to find cross-boundary subarrays.
Hint 2: The four fields
Each node stores: total (sum of range), prefix (max prefix sum), suffix (max suffix sum), best (max subarray sum). A leaf with value a: all four = a (or max(0, a) if empty subarrays count as 0).
Hint 3: The merge formula
total = L.total + R.totalprefix = max(L.prefix, L.total + R.prefix)suffix = max(R.suffix, R.total + L.suffix)best = max(L.best, R.best, L.suffix + R.prefix)
The last line catches subarrays that cross the midpoint.
Codeforces 380C — Sereja and Brackets
Query the length of the longest valid parentheses subsequence in a range. Classic custom-merge problem.
Hint 1: What each node stores
After greedily matching brackets in a range, store open = unmatched ( count, close = unmatched ) count. The matched count = total_chars − open − close.
Hint 2: How to merge
Unmatched ( from the left can match unmatched ) from the right. matched = min(L.open, R.close). Then: open = L.open + R.open − matched, close = L.close + R.close − matched.
Hint 3: Getting the answer
For query [l, r] with length len: answer = len − result.open − result.close. This counts characters that are part of matched pairs.
For a range, find the GCD and count elements that don’t divide the GCD (ants that leave).
Hint 1: Two fields per node
Store {gcd, count} where gcd = GCD of range, count = elements equal to the gcd. Only these are guaranteed to divide every element in the range.
Hint 2: Merge logic
Compute g = gcd(L.gcd, R.gcd). If g == L.gcd, inherit L’s count. If g == R.gcd, inherit R’s count. If g equals both, add counts. If g is smaller than both, count = 0.
Hint 3: Computing the answer
Query [l, r] returns {gcd, count}. Ants that leave = (r − l + 1) − count. The staying ants are those whose strength equals the range GCD.
Library Checker — Point Set Range Composite
Each element is a linear function f(x) = ax + b. Query the composition over a range. Order matters — non-commutative merge!
Hint 1: Composing two linear functions
f(x)=a₁x+b₁ and g(x)=a₂x+b₂. Then f(g(x)) = a₁(a₂x+b₂)+b₁ = (a₁·a₂)x + (a₁·b₂+b₁). Store each node as the pair (a, b).
Hint 2: Non-commutative means order matters
f(g(x)) ≠ g(f(x)) in general. Your merge must apply left-child first, then right-child consistently. Be careful in build, update, AND query to maintain left-to-right order.
Hint 3: Identity and mod arithmetic
Identity function: (a=1, b=0) since f(x) = 1·x + 0 = x. Return this for no-overlap cases. All arithmetic is mod 998244353 — cast to long long before multiplying.
Advanced: Persistent & Offline
Find the kth smallest element in any subarray [l, r]. The canonical persistent segment tree problem.
Hint 1: What a persistent segment tree is
A persistent tree keeps all previous versions. When you “update,” you create a new root sharing most nodes with the old tree (only the root-to-leaf path is new). Version i = tree after inserting the first i elements.
Hint 2: Build versions left to right
Coordinate compress values. Process positions 1..n. Version i = version i−1 + a +1 at compressed position of a[i]. Now version[r] − version[l−1] gives the frequency distribution for just [l, r].
Hint 3: Walking the tree for kth smallest
Start at roots of version[r] and version[l−1]. At each node, left_count = ver_r.left.count − ver_l1.left.count. If left_count ≥ k, go left. Otherwise k −= left_count, go right. This binary searches the value range.
CSES — Distinct Values Queries
Count distinct values in [l, r]. Best solved offline with a sweep.
Hint 1: The offline trick
Sort queries by right endpoint. Sweep the array left to right, maintaining a segment tree. Answer each query when you reach its right boundary.
Hint 2: Track rightmost occurrence only
For each value, track its last seen position. At position i: if a[i] was last at position j, do update(j, −1) then update(i, +1). Each value has exactly one “1” in the tree, at its rightmost occurrence.
Hint 3: Answering queries
After processing up to r, every distinct value in [1, r] has a 1 at its rightmost position. Answer for [l, r] = query(l, r) = count of 1s in that range = distinct values with rightmost occurrence in [l, r].
Codeforces 600E — Lomsat gelral
For each vertex in a tree, find the sum of the most frequent colors in its subtree. Uses “DSU on tree” (small-to-large merging).
Hint 1: The brute-force view
For each subtree, you need every color’s frequency, then sum colors with max frequency. Brute-force = O(n²). The trick: reuse the heavy child’s data instead of recomputing.
Hint 2: DSU on tree (small-to-large)
Pick the “heavy child” (largest subtree). Keep its frequency data, don’t clear it. Re-add all elements from light children one by one. Each element is re-added O(log n) times total across the whole tree → O(n log n).
Hint 3: Implementation
Global cnt[] for color frequencies, max_freq, sum_of_max. Adding color c: cnt[c]++. If cnt[c] > max_freq, update max and reset sum to c. If cnt[c] == max_freq, add c to sum. Removing: reverse the logic.
Summary
- Pattern 1: Range sum + point update, the basic template. Drop-in for many counting/inversion problems.
- Pattern 2: Range min/max, change merge to
min/max, identity to ∞/-∞. - Pattern 3: Range update + range query, requires lazy propagation.
- Pattern 4: Custom merges, store complex state per node, merge in O(1).
- Pattern 5: Coordinate compression, map large values to small ranks.
- When there are no updates, prefer prefix sums or sparse tables.
- When only sum + point update is needed, Fenwick tree is faster and simpler.
- Segment trees shine with min/max queries, range updates, and complex merge logic.