← All Posts
DSA Series · Trees · Segment Trees · Part 5

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

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:

Classic Problems

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

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 StoresMerge LogicUse Case
{sum, max, min}Component-wise combineMultiple stats in one pass
{prefix_sum, suffix_sum, total, max_subarray}Kadane-style mergeMax subarray sum with updates
{count_zeros, count_ones}AdditionCount occurrences with flips
{matrix 2×2}Matrix multiplicationFibonacci range queries

Example: Maximum Subarray Sum (LeetCode 53 variant with updates)

Each node stores four values:

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:

  1. Collect all values, sort and deduplicate.
  2. Map each value to its rank (0, 1, 2, ...).
  3. 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

Segment Tree vs. Fenwick Tree (BIT)

Segment TreeFenwick Tree (BIT)
Range queryO(log n)O(log n)
Point updateO(log n)O(log n)
Range updateO(log n) with lazyO(log n) for add (with trick)
Min/Max query✘ (only prefix min/max)
Custom merge
Code length~30-50 lines~15-20 lines
Memory4nn
Constant factorLargerSmaller (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.

CSES — Xor Queries

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

CSES — Range Update Queries

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.

CSES — Range Updates and Sums

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).

Codeforces 52C — Circular RMQ

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).

LeetCode 493 — Reverse Pairs

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).

CSES — Salary Queries

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.

CSES — Nested Ranges Count

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

CSES — Subarray Sum 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.total
prefix = 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.

Codeforces 474F — Ant Colony

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

SPOJ — MKTHNUM

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.

More practice: The USACO Guide — Segment Tree (Platinum) and CP-Algorithms: Segment Tree pages maintain comprehensive problem lists with difficulty tags. For lazy propagation specifically, see this classic Codeforces tutorial.

Summary