How To Solve ANY Segment Tree Problem - Step By Step Guide Along With Problem Examples

Segment trees look intimidating at first.

But once you see the structure, every problem uses
the same skeleton — only the merge operation changes.

Here is the complete guide.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WHEN TO USE A SEGMENT TREE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Use a segment tree when you need BOTH:
→ Range queries (sum/min/max over [l, r])
→ Updates (to elements or ranges)

If no updates → use prefix sum (simpler, O(1) query)
If only point updates + range sum → Fenwick Tree (BIT) is simpler
If range min/max queries → segment tree (BIT cannot do this)
If range updates → segment tree with lazy propagation

Keywords: "range sum/min/max after updates", "mutable",
"update range", "query range"

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
HOW THE STRUCTURE WORKS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

A segment tree is a binary tree stored in an array of size 4*n.

tree[1] = root → stores answer for the full array [0, n-1]
tree[2i] = left child (covers left half)
tree[2
i + 1] = right child (covers right half)
Leaves = individual elements

Each internal node stores the MERGED answer of its two children.
What "merged" means depends on the problem (sum, min, max, etc.).

Index:       1
           /   \
          2     3
         / \   / \
        4   5 6   7   ← leaf nodes = nums[0..3]
Build:  O(n)       — fill all nodes bottom-up
Update: O(log n)update one path from leaf to root
Query:  O(log n)   — combine at most O(log n) nodes

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
THERE ARE 3 TYPES OF SEGMENT TREE PROBLEMS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

TYPE 1 — POINT UPDATE + RANGE SUM QUERY
TYPE 2 — POINT UPDATE + RANGE MIN/MAX QUERY
TYPE 3 — RANGE UPDATE + RANGE QUERY (Lazy Propagation)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TYPE 1 — POINT UPDATE + RANGE SUM QUERY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

When to use:
→ Update a single index
→ Query the sum of any range [l, r]

THE TEMPLATE:

struct SegTree {
    int n;
    vector<long long> tree;

    SegTree(int n) : n(n), tree(4 * n, 0) {}

    void build(vector<int>& nums, int node, int lo, int hi) {
        if (lo == hi) { tree[node] = nums[lo]; return; }
        int mid = lo + (hi - lo) / 2;
        build(nums, 2*node,   lo,    mid);
        build(nums, 2*node+1, mid+1, hi);
        tree[node] = tree[2*node] + tree[2*node+1];   // merge = sum
    }

    void update(int node, int lo, int hi, int idx, long long val) {
        if (lo == hi) { tree[node] = val; return; }
        int mid = lo + (hi - lo) / 2;
        if (idx <= mid) update(2*node,   lo,    mid, idx, val);
        else update(2*node+1, mid+1, hi,  idx, val);
        tree[node] = tree[2*node] + tree[2*node+1];   // update merge
    }

    long long query(int node, int lo, int hi, int l, int r) {
        if (r < lo || hi < l) return 0;               // out of range
        if (l <= lo && hi <= r) return tree[node];    // fully in range
        int mid = lo + (hi - lo) / 2;
        return query(2*node,   lo,    mid, l, r) +
               query(2*node+1, mid+1, hi,  l, r);
    }
};
// Usage:
SegTree st(n);
st.build(nums, 1, 0, n-1);
st.update(1, 0, n-1, idx, newVal);
long long ans = st.query(1, 0, n-1, l, r);

The three query cases (memorize these):

r < lo || hi < l   → segment is completely outside query rangereturn 0
l <= lo && hi <= r  → segment is completely inside query rangereturn tree[node]
otherwise           → partial overlap → recurse on both children, sum results

EXAMPLE — Range Sum Query Mutable
https://leetcode.com/problems/range-sum-query-mutable

Exactly the template above. Build once, then handle two operations:
update(i, val) → point update
sumRange(l, r) → range sum query

class NumArray {
    SegTree st;
    int n;
public:
    NumArray(vector<int>& nums) : n(nums.size()), st(nums.size()) {
        st.build(nums, 1, 0, n-1);
    }
    void update(int i, int val) { st.update(1, 0, n-1, i, val); }
    int sumRange(int l, int r)  { return st.query(1, 0, n-1, l, r); }
};

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TYPE 2 — POINT UPDATE + RANGE MIN/MAX QUERY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

When to use:
→ Same as Type 1 but query asks for minimum or maximum, not sum

Only TWO lines change from the sum template:
The merge operation: min/max instead of +
The out-of-range return value: INT_MAX (for min) or INT_MIN (for max)

RANGE MIN TEMPLATE (only the changed lines):

// out of rangereturn INT_MAX (neutral for min)
if (r < lo || hi < l) return INT_MAX;

// merge = take min of two children
tree[node] = min(tree[2*node], tree[2*node+1]);

RANGE MAX TEMPLATE:

// out of rangereturn INT_MIN (neutral for max)
if (r < lo || hi < l) return INT_MIN;

// merge = take max of two children
tree[node] = max(tree[2*node], tree[2*node+1]);

EXAMPLE — Falling Squares
https://leetcode.com/problems/falling-squares

Each square drops onto the tallest existing square below it.
After placing each square, query max height in its range.
Then update that range with the new max height.

Coordinate compression: positions can be huge (up to 10^8),
but there are at most 2 * squares.size() distinct x-coordinates.
Map them to indices 0..m-1, build segment tree of size m.

EXAMPLE — Sliding Window Maximum
https://leetcode.com/problems/sliding-window-maximum

Can be solved with a deque (simpler), but range max segment tree also works.
For each window [l, r], query max in [l, r].

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TYPE 3 — RANGE UPDATE + RANGE QUERY (LAZY PROPAGATION)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

When to use:
→ Add/set a value to ALL elements in a range
→ Then query sum/min/max of a range

The problem with naive range update: updating every element in [l, r]
individually takes O(n) per update → too slow.

Lazy propagation defers updates to children until they are actually needed.
Each node stores a "lazy" value: a pending update not yet pushed to children.

THE KEY IDEA:

When updating a range: if the current node's range is fully covered,
update this node and store the pending value in lazy[node].
Do NOT go to the children yet.

When you DO need to visit children (query or partial update):
PUSH DOWN the lazy value to children first.

LAZY TEMPLATE (range add + range sum):

struct LazySegTree {
    int n;
    vector<long long> tree, lazy;

    LazySegTree(int n) : n(n), tree(4*n, 0), lazy(4*n, 0) {}

    void pushDown(int node, int lo, int hi) {
        if (lazy[node] == 0) return;
        int mid = lo + (hi - lo) / 2;
        // push lazy to left child
        tree[2*node]   += lazy[node] * (mid - lo + 1);
        lazy[2*node]   += lazy[node];
        // push lazy to right child
        tree[2*node+1] += lazy[node] * (hi - mid);
        lazy[2*node+1] += lazy[node];
        // clear lazy at current node
        lazy[node] = 0;
    }

    void update(int node, int lo, int hi, int l, int r, long long val) {
        if (r < lo || hi < l) return;
        if (l <= lo && hi <= r) {
            tree[node] += val * (hi - lo + 1);   // update sum for this range
            lazy[node] += val;                    // defer to children
            return;
        }
        pushDown(node, lo, hi);                  // push before going deeper
        int mid = lo + (hi - lo) / 2;
        update(2*node,   lo,    mid, l, r, val);
        update(2*node+1, mid+1, hi,  l, r, val);
        tree[node] = tree[2*node] + tree[2*node+1];
    }

    long long query(int node, int lo, int hi, int l, int r) {
        if (r < lo || hi < l) return 0;
        if (l <= lo && hi <= r) return tree[node];
        pushDown(node, lo, hi);                  // push before going deeper
        int mid = lo + (hi - lo) / 2;
        return query(2*node,   lo,    mid, l, r) +
               query(2*node+1, mid+1, hi,  l, r);
    }
};

WHY tree[node] += val * (hi - lo + 1)?
tree[node] stores the SUM of its range.
If you add val to every element in a range of size (hi-lo+1),
the sum increases by val * (hi - lo + 1).

EXAMPLE — My Calendar III
https://leetcode.com/problems/my-calendar-iii

Book events as range adds of +1. Query max overlap in any range.
This is range add + range max query with lazy propagation.

For range max lazy:

    tree[node] = max value in range.
    lazy[node] = pending add to all children.
    pushDown: tree[child] += lazy[node], lazy[child] += lazy[node].
    merge: tree[node] = max(tree[2*node], tree[2*node+1]).

EXAMPLE — Range Module
https://leetcode.com/problems/range-module

Track which ranges are "tracked" (1) or not (0).
addRange → range set to 1
removeRange → range set to 0
queryRange → check if all values in range are 1

This uses range assign (set) lazy, not range add.
lazy[node] stores the value to assign (-1 = no pending assignment).

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
COORDINATE COMPRESSION — WHEN VALUES ARE HUGE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Problem: positions can be up to 10^9 but there are only n queries.
Solution: map the distinct values to indices 0..m-1.

vector<int> coords;
for (auto& q : queries) {
    coords.push_back(q[0]);
    coords.push_back(q[1]);
}
sort(coords.begin(), coords.end());
coords.erase(unique(coords.begin(), coords.end()), coords.end());

// map original value to compressed index
auto getIdx = [&](int val) {
    return lower_bound(coords.begin(), coords.end(), val) - coords.begin();
};

// build segment tree of size coords.size()

Use this when: positions are large but number of distinct positions is small.
Problems: Falling Squares, Range Module, Count of Smaller Numbers After Self.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
HOW TO IDENTIFY WHICH TYPE — DECISION GUIDE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  • Do you need range updates (update a whole range at once)?
    → YES → Type 3 (Lazy Propagation)
    → NO → Type 1 or Type 2

  • What does your query return?
    → Sum → Type 1 (range sum + point update)
    → Min → Type 2 (range min, return INT_MAX for out-of-range)
    → Max → Type 2 (range max, return INT_MIN for out-of-range)

  • Are the coordinates huge (up to 10^8 or 10^9)?
    → YES → use coordinate compression first

  • Could a simpler structure work?
    → No updates at all → prefix sum array (O(1) query)
    → Point update + range sum → Fenwick/BIT (simpler code)
    → Range update or min/max → segment tree is the right tool

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
COMMON MISTAKES AND HOW TO AVOID THEM
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  • MISTAKE 1 — Tree size too small
    Always allocate tree of size 4n, not 2n.
    2n only works for perfect binary trees.
    4n is always safe.

  • MISTAKE 2 — Forgetting pushDown before recursing into children
    In lazy propagation, before you recurse into children
    (for either update or query), you MUST call pushDown first.
    Without it, children have stale values and queries return wrong results.

  • MISTAKE 3 — Wrong out-of-range return value
    For sum → return 0 (neutral element for addition)
    For min → return INT_MAX (neutral element for min)
    For max → return INT_MIN (neutral element for max)
    Returning 0 for a min query will incorrectly affect the result.

  • MISTAKE 4 — Wrong lazy update formula
    For range add + sum tree:
    tree[node] += lazy * range_size (not just += lazy)
    The sum of a range increases by lazy * (number of elements in range).

  • MISTAKE 5 — Using 0-indexed vs 1-indexed inconsistently
    This template uses 1-indexed nodes (root = node 1).
    The input array is 0-indexed (leaves cover [0, n-1]).
    The node indices and the array indices are different things.
    Don't confuse them.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRACTICE PROBLEMS — IN ORDER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
THE SEGMENT TREE CHEAT SHEET — ONE LINE EACH
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  • Always allocate 4*n nodes

  • Root = node 1, left = 2i, right = 2i+1

  • Three query cases: outside → neutral, inside → return, partial → split

  • Change the merge and neutral value to switch between sum/min/max

  • Lazy propagation → pushDown before recursing into children

  • Range add to sum tree → tree[node] += lazy * (hi - lo + 1)

  • Huge coordinates → compress to indices first

  • No updates needed → prefix sum

  • Sum + point updates → Fenwick Tree (BIT) is simpler

  • Min/max queries → segment tree

  • Range updates → segment tree + lazy propagation

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Drop a comment if anything is unclear.
I will write a dedicated post on any type you want.

Comments (4)