πŸ“– Chapter 3.9 ⏱️ ~70 min 🎯 Advanced

Chapter 3.9: Introduction to Segment Trees

πŸ“ Prerequisites: Understanding of prefix sums (Chapter 3.2), arrays and recursion (Chapter 2.3). Segment Trees are an advanced data structure β€” make sure you're comfortable with recursion before diving in.

Segment Trees are one of the most powerful data structures in competitive programming, solving the fundamental problem that prefix sums cannot handle: range queries with updates.


3.9.1 The Problem: Why Do We Need Segment Trees?

Consider this challenge:

  • Array A with N integers
  • Q1: What is the sum of A[l..r]? (range sum query)
  • Q2: Update A[i] = x (point update)

Prefix Sum Approach: Range query O(1), but updates require recomputing all prefix sums, O(N). For M mixed queries, total O(NM) β€” too slow when N, M = 10^5.

Segment Tree Approach: Both query and update are O(log N). M mixed queries total: O(M log N) βœ“

Data StructureBuildQueryUpdateBest For
Plain ArrayO(N)O(N)O(1)Updates only
Prefix SumO(N)O(1)O(N)Queries only
Segment TreeO(N)O(log N)O(log N)Queries + Updates
Fenwick Tree (BIT)O(N log N)O(log N)O(log N)Simpler code, prefix sums only

The diagram above shows a Segment Tree built on array [1, 3, 5, 7, 9, 11]. Each internal node stores the sum of its interval. A query on interval [2,4] (sum=21) only needs to combine 2 nodes β€” O(log N) instead of O(N).


3.9.2 Structure: What Is a Segment Tree?

A Segment Tree is a complete binary tree where:

  • Each leaf node corresponds to one array element
  • Each internal node stores the aggregate value (sum, min, max, etc.) of its interval
  • The root covers the entire array [0..N-1]
  • A node covering [l..r] has two children: [l..mid] and [mid+1..r]

For an array of N elements, the tree has at most 4N nodes (we use a 1-indexed tree array of size 4N as a safe upper bound).

Array: [1, 3, 5, 7, 9, 11] (indices 0..5)

Tree (1-indexed, node i's children are 2i and 2i+1):
         [0..5]=36
        /          \
  [0..2]=9       [3..5]=27
   /     \        /      \
[0..1]=4 [2]=5  [3..4]=16  [5]=11
  /   \          /    \
[0]=1 [1]=3   [3]=7  [4]=9

The diagram below shows the complete Segment Tree structure, with the blue-highlighted access path for query sum([2,4]):

Segment Tree Structure


3.9.3 Building the Segment Tree

πŸ“„ View Code: 3.9.3 Building the Segment Tree
// Build Segment Tree β€” O(N)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
int tree[4 * MAXN];  // Segment tree array (4x array length)
int arr[MAXN];       // Original array

// Build: recursively fill tree[]
// node = current tree node index (1-indexed)
// start, end = range covered by this node
void build(int node, int start, int end) {
    if (start == end) {
        // Leaf node: store array element
        tree[node] = arr[start];
    } else {
        int mid = (start + end) / 2;
        // Build left and right children first
        build(2 * node, start, mid);        // Left child
        build(2 * node + 1, mid + 1, end);  // Right child
        // Internal node: sum of children
        tree[node] = tree[2 * node] + tree[2 * node + 1];
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;
    for (int i = 0; i < n; i++) cin >> arr[i];

    build(1, 0, n - 1);  // Build from node 1, covering [0..n-1]

    return 0;
}

Build trace for [1, 3, 5, 7, 9, 11]:

πŸ“„ Full Code
build(1, 0, 5):
  build(2, 0, 2):
    build(4, 0, 1):
      build(8, 0, 0): tree[8] = arr[0] = 1
      build(9, 1, 1): tree[9] = arr[1] = 3
      tree[4] = tree[8] + tree[9] = 4
    build(5, 2, 2): tree[5] = arr[2] = 5
    tree[2] = tree[4] + tree[5] = 9
  build(3, 3, 5):
    ...
    tree[3] = 27
  tree[1] = 9 + 27 = 36

3.9.4 Range Query

Query the sum of arr[l..r]:

Core Idea: Recursively descend the tree; at each node covering [start..end]:

  • If [start..end] is completely inside [l..r]: return this node's value directly (done!)
  • If [start..end] is completely outside [l..r]: return 0 (no contribution)
  • Otherwise: recurse into both children and sum the results
πŸ“„ Full C++ Code
// Range Query: sum of arr[l..r] β€” O(log N)
// node = current tree node, [start, end] = covered range
// [l, r] = query range
int query(int node, int start, int end, int l, int r) {
    if (r < start || end < l) {
        // Case 1: current interval completely outside query range
        return 0;   // Identity element for sum (use INT_MAX for min)
    }
    if (l <= start && end <= r) {
        // Case 2: current interval completely inside query range
        return tree[node];   // ← Key line: use this node directly!
    }
    // Case 3: partial overlap β€” recurse into children
    int mid = (start + end) / 2;
    int leftSum  = query(2 * node, start, mid, l, r);
    int rightSum = query(2 * node + 1, mid + 1, end, l, r);
    return leftSum + rightSum;
}

// Usage: sum of arr[2..4]
int result = query(1, 0, n - 1, 2, 4);
cout << result << "\n";  // 5 + 7 + 9 = 21

Query trace for [2..4] on tree of [1,3,5,7,9,11]:

query(1, 0, 5, 2, 4):
  query(2, 0, 2, 2, 4): [0..2] partially overlaps [2..4]
    query(4, 0, 1, 2, 4): [0..1] outside [2..4] β†’ return 0
    query(5, 2, 2, 2, 4): [2..2] inside [2..4] β†’ return 5
    return 0 + 5 = 5
  query(3, 3, 5, 2, 4): [3..5] partially overlaps [2..4]
    query(6, 3, 4, 2, 4): [3..4] inside [2..4] β†’ return 16
    query(7, 5, 5, 2, 4): [5..5] outside [2..4] β†’ return 0
    return 16 + 0 = 16
  return 5 + 16 = 21 βœ“

Only 4 nodes visited β€” O(log N)!

The diagram below shows which nodes are visited and why β€” green nodes return their value directly, orange nodes recurse into children, gray nodes are immediately pruned:

Segment Tree Query Visualization


3.9.5 Point Update

Update arr[i] = x (modify a single element):

πŸ“„ Update `arr[i] = x` (modify a single element):
// Point Update: set arr[idx] = val β€” O(log N)
void update(int node, int start, int end, int idx, int val) {
    if (start == end) {
        // Leaf node: update value
        arr[idx] = val;
        tree[node] = val;
    } else {
        int mid = (start + end) / 2;
        if (idx <= mid) {
            update(2 * node, start, mid, idx, val);      // Update in left subtree
        } else {
            update(2 * node + 1, mid + 1, end, idx, val); // Update in right subtree
        }
        // After child changes, update this internal node
        tree[node] = tree[2 * node] + tree[2 * node + 1];
    }
}

// Usage: set arr[2] = 10
update(1, 0, n - 1, 2, 10);

A point update only modifies nodes on the path from the updated leaf to the root β€” only O(log N) nodes, all other branches remain unchanged:

Segment Tree Point Update


3.9.6 Complete Implementation

Here is a complete, contest-ready Segment Tree:

πŸ“„ Complete contest-ready Segment Tree:
// Segment Tree β€” O(N) build, O(log N) query/update
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
long long tree[4 * MAXN];

void build(int node, int start, int end, long long arr[]) {
    if (start == end) {
        tree[node] = arr[start];
        return;
    }
    int mid = (start + end) / 2;
    build(2 * node, start, mid, arr);
    build(2 * node + 1, mid + 1, end, arr);
    tree[node] = tree[2 * node] + tree[2 * node + 1];
}

long long query(int node, int start, int end, int l, int r) {
    if (r < start || end < l) return 0;
    if (l <= start && end <= r) return tree[node];
    int mid = (start + end) / 2;
    return query(2 * node, start, mid, l, r)
         + query(2 * node + 1, mid + 1, end, l, r);
}

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

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n, q;
    cin >> n >> q;
    long long arr[MAXN];
    for (int i = 0; i < n; i++) cin >> arr[i];

    build(1, 0, n - 1, arr);

    while (q--) {
        int type;
        cin >> type;
        if (type == 1) {
            // Point update: set arr[i] = v
            int i; long long v;
            cin >> i >> v;
            update(1, 0, n - 1, i, v);
        } else {
            // Range query: sum of arr[l..r]
            int l, r;
            cin >> l >> r;
            cout << query(1, 0, n - 1, l, r) << "\n";
        }
    }

    return 0;
}

Sample Input:

6 5
1 3 5 7 9 11
2 2 4
1 2 10
2 2 4
2 0 5
1 0 0

Sample Output:

21
26
41

(1st query [2,4] = 5+7+9 = 21; after update arr[2]=10, 2nd query [2,4] = 10+7+9 = 26; 3rd query [0,5] = 1+3+10+7+9+11 = 41)


3.9.7 Segment Tree vs Fenwick Tree (BIT)

FeatureSegment TreeFenwick Tree (BIT)
Code ComplexityMedium (~30 lines)Simple (~15 lines)
Range QueryAny associative operationPrefix sums only
Range UpdateYes (with lazy propagation)Yes (with tricks)
Point UpdateO(log N)O(log N)
SpaceO(4N)O(N)
Use CaseRange min/max, complex queriesPrefix sums with updates

πŸ’‘ Core Insight: For range sums with updates, BIT is simpler. For range minimum/maximum, or any non-prefix operation, use Segment Tree.


3.9.8 Range Minimum Query Variant

Just change the aggregate operation from + to min:

πŸ“„ Just change the aggregate operation from `+` to `min`:
// Range Minimum Segment Tree β€” same structure, different operation
void build_min(int node, int start, int end, int arr[]) {
    if (start == end) { tree[node] = arr[start]; return; }
    int mid = (start + end) / 2;
    build_min(2*node, start, mid, arr);
    build_min(2*node+1, mid+1, end, arr);
    tree[node] = min(tree[2*node], tree[2*node+1]);  // ← Changed to min
}

int query_min(int node, int start, int end, int l, int r) {
    if (r < start || end < l) return INT_MAX;   // ← Identity element for min
    if (l <= start && end <= r) return tree[node];
    int mid = (start + end) / 2;
    return min(query_min(2*node, start, mid, l, r),
               query_min(2*node+1, mid+1, end, l, r));
}

3.9.9 Range Update with Lazy Propagation

The previous Segment Tree handles point updates. What about range updates: "add V to all elements in [L, R]"?

Without lazy propagation, you'd need O(N) updates (one per element). With lazy propagation, achieve O(log N) range updates.

Segment Tree Lazy Propagation

πŸ’‘ Core Idea: Instead of immediately updating all affected leaf nodes, "lazily" defer updates β€” store the update at the highest applicable node, and only push it down to children when they're actually needed.

Each node now stores two values:

  • tree[node]: the actual aggregate value of the interval (range sum)
  • lazy[node]: pending update not yet pushed to children

Push-down rule: When visiting a node with a pending lazy update:

  1. Apply the lazy update to this node's value
  2. Pass the lazy update to both children (push down)
  3. Clear this node's lazy value
πŸ“„ 3. Clear this node's lazy value
// Segment Tree with Lazy Propagation
// Supports: range add update, range sum query β€” each O(log N)
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const int MAXN = 100005;

ll tree[4 * MAXN];   // tree[node] = range sum
ll lazy[4 * MAXN];   // lazy[node] = pending add value (0 = no pending)

// ── Push Down: pass pending lazy to children ──
void pushDown(int node, int start, int end) {
    if (lazy[node] == 0) return;  // No pending update

    int mid = (start + end) / 2;
    int left = 2 * node, right = 2 * node + 1;

    // Update left child's sum: add lazy * (number of elements in left child)
    tree[left]  += lazy[node] * (mid - start + 1);
    tree[right] += lazy[node] * (end - mid);

    // Pass lazy to children
    lazy[left]  += lazy[node];
    lazy[right] += lazy[node];

    // Clear current node's lazy (already pushed down)
    lazy[node] = 0;
}

// ── Build ──
void build(int node, int start, int end, ll arr[]) {
    lazy[node] = 0;
    if (start == end) {
        tree[node] = arr[start];
        return;
    }
    int mid = (start + end) / 2;
    build(2*node, start, mid, arr);
    build(2*node+1, mid+1, end, arr);
    tree[node] = tree[2*node] + tree[2*node+1];
}

// ── Range Update: add val to all elements in [l, r] ──
void update(int node, int start, int end, int l, int r, ll val) {
    if (r < start || end < l) return;  // Out of range: no-op

    if (l <= start && end <= r) {
        // Current interval completely inside [l, r]: apply lazy, don't recurse
        tree[node] += val * (end - start + 1);  // ← Key: multiply by interval length
        lazy[node] += val;                        // Store pending value for children
        return;
    }

    // Partial overlap: push down existing lazy first, then recurse
    pushDown(node, start, end);  // ← Key: must pushDown before recursing!

    int mid = (start + end) / 2;
    update(2*node,   start, mid, l, r, val);
    update(2*node+1, mid+1, end, l, r, val);

    // Update current node from children
    tree[node] = tree[2*node] + tree[2*node+1];
}

// ── Range Query: sum of elements in [l, r] ──
ll query(int node, int start, int end, int l, int r) {
    if (r < start || end < l) return 0;  // Out of range

    if (l <= start && end <= r) {
        return tree[node];  // Completely inside: return stored sum (already includes lazy!)
    }

    // Partial overlap: push down first, then recurse
    pushDown(node, start, end);  // ← Key: must pushDown before recursing!

    int mid = (start + end) / 2;
    ll leftSum  = query(2*node,   start, mid, l, r);
    ll rightSum = query(2*node+1, mid+1, end, l, r);
    return leftSum + rightSum;
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n, q;
    cin >> n >> q;

    ll arr[MAXN];
    for (int i = 0; i < n; i++) cin >> arr[i];

    build(1, 0, n-1, arr);

    while (q--) {
        int type;
        cin >> type;

        if (type == 1) {
            // Range update: add val to [l, r]
            int l, r; ll val;
            cin >> l >> r >> val;
            update(1, 0, n-1, l, r, val);
        } else {
            // Range query: sum of [l, r]
            int l, r;
            cin >> l >> r;
            cout << query(1, 0, n-1, l, r) << "\n";
        }
    }

    return 0;
}

⚠️ Common Lazy Propagation Bugs

Top 4 Lazy Propagation Bugs:

  1. Forgetting pushDown before recursing β€” children will receive their own lazy on top of parent's, causing wrong query results
  2. Wrong size multiplier β€” writing tree[node] += val instead of tree[node] += val * (end - start + 1). The node stores a sum, and adding val to (end-start+1) elements means the sum increases by val Γ— size
  3. Not initializing lazy[] to 0 β€” use memset(lazy, 0, sizeof(lazy)) or initialize in build()
  4. Mixing different lazy operations β€” if you have both "range add" and "range multiply" lazy, order matters; need two separate lazy arrays and careful pushDown handling

Generalizing Lazy Propagation

This pattern works for any operation satisfying:

  • Aggregation is associative (sum, min, max, XOR...)
  • Updates distribute over aggregation (adding k to n elements increases sum by k*n)
UpdateQueryLazy StorespushDown Formula
Range addRange sumAdd deltatree[child] += lazy * size; lazy[child] += lazy
Range assignRange sumAssign valuetree[child] = lazy * size; lazy[child] = lazy
Range addRange minAdd deltatree[child] += lazy; lazy[child] += lazy
Range assignRange minAssign valuetree[child] = lazy; lazy[child] = lazy

3.9.10 Range Assignment (Second Type of Lazy)

Range add is the most common lazy operation, but contests also feature: set all elements in [L, R] to the same value V.

The difference is in the pushDown logic: range add lazy is "incremental accumulation", range assign lazy is "direct overwrite".

πŸ“„ The difference is in the pushDown logic: range add is "incremental", range assign is "direct overwrite".
// Segment Tree with Range Assignment Lazy
// tree[i] = range sum, lazy[i] = assign marker (-1 = no marker)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100005;

ll tree[4*MAXN];
ll lazy[4*MAXN];  // -1 = no marker

void build(int s, int t, int p, ll a[]) {
    lazy[p] = -1;
    if (s == t) { tree[p] = a[s]; return; }
    int m = s + ((t - s) >> 1);
    build(s, m, p*2, a);
    build(m+1, t, p*2+1, a);
    tree[p] = tree[p*2] + tree[p*2+1];
}

// ── pushDown: range assignment ──
void pushDown(int p, int s, int t) {
    if (lazy[p] == -1) return;
    int m = s + ((t - s) >> 1);
    // Assign lazy[p] to all elements in left child, count = m-s+1
    tree[p*2]   = lazy[p] * (m - s + 1);
    tree[p*2+1] = lazy[p] * (t - m);
    lazy[p*2]   = lazy[p];   // Overwrite (not accumulate!)
    lazy[p*2+1] = lazy[p];
    lazy[p] = -1;             // Clear marker
}

// ── Range assignment update ──
void update(int l, int r, ll c, int s, int t, int p) {
    if (l <= s && t <= r) {
        tree[p] = c * (t - s + 1);  // Assign entire segment
        lazy[p] = c;
        return;
    }
    pushDown(p, s, t);
    int m = s + ((t - s) >> 1);
    if (l <= m) update(l, r, c, s, m, p*2);
    if (r > m)  update(l, r, c, m+1, t, p*2+1);
    tree[p] = tree[p*2] + tree[p*2+1];
}

// ── Range sum query (same as range add version, pushDown first) ──
ll query(int l, int r, int s, int t, int p) {
    if (l <= s && t <= r) return tree[p];
    pushDown(p, s, t);
    int m = s + ((t - s) >> 1);
    ll res = 0;
    if (l <= m) res += query(l, r, s, m, p*2);
    if (r > m)  res += query(l, r, m+1, t, p*2+1);
    return res;
}

⚠️ Key Difference Between Range Assign and Range Add: In pushDown, assignment uses overwrite (lazy[child] = val), addition uses accumulation (lazy[child] += val). If mixing both operations, maintain two separate lazy arrays and handle priority carefully.


3.9.11 Dynamic Node Allocation Segment Tree

Use Cases

When the value domain is very large (e.g., $10^9$), you can't preallocate a 4N array. But if the number of operations M is small (e.g., $10^5$), only $O(M \log V)$ nodes will actually be visited.

Core Idea: Nodes are only created when accessed; use ls[p], rs[p] to record left/right child indices (replacing 2p/2p+1).

πŸ“„ Full C++ Code
// Dynamic Node Allocation Segment Tree (Weight/Value Segment Tree)
// Typical use: range counting, K-th smallest
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2e6 + 5;  // Node pool limit (M * log V)

int ls[MAXN], rs[MAXN];     // Left/right child indices
long long sum[MAXN];
int cnt, root;              // Node counter, root index

// Add 1 at position x (for inserting value x into weight segment tree)
void update(int &p, int s, int t, int x) {
    if (!p) p = ++cnt;          // Dynamically create node if it doesn't exist
    sum[p]++;
    if (s == t) return;
    int m = s + ((t - s) >> 1);
    if (x <= m) update(ls[p], s, m, x);
    else        update(rs[p], m+1, t, x);
}

// Range sum query
long long query(int p, int s, int t, int l, int r) {
    if (!p) return 0;           // Node doesn't exist, no elements in this range
    if (l <= s && t <= r) return sum[p];
    int m = s + ((t - s) >> 1);
    long long res = 0;
    if (l <= m) res += query(ls[p], s, m, l, r);
    if (r > m)  res += query(rs[p], m+1, t, l, r);
    return res;
}

// Usage example: insert values then query count in [l, r]
// update(root, 1, 1e9, val);
// query(root, 1, 1e9, l, r);

Space Complexity: After M operations, node count is $O(M \log V)$, much less than $4V$.


3.9.12 Segment Tree Optimized Graph Construction

Use Cases

In graph problems, if you need "one node connects to all nodes in a range" or "all nodes in a range connect to one node", naive construction has $O(N^2)$ edges; using Segment Trees reduces this to $O(N \log N)$.

Approach

Build two Segment Trees:

TreeDirectionDescription
Out-tree (range→node)Child→Parent (0-weight edges)Leaf nodes connect to original graph nodes, interval nodes aggregate to parent
In-tree (node→range)Parent→Child (0-weight edges)Parent distributes to leaf nodes, leaf nodes connect to original graph nodes
Range [2,4] β†’ node u edge:
  In the in-tree, the [2,4] interval node connects with weight w to u
  In-tree internal parent→child edges have weight 0, leaf nodes coincide with original graph nodes

Node u β†’ range [2,4] edge:
  In the out-tree, u connects with weight w to the [2,4] interval node
  Out-tree internal child→parent edges have weight 0, leaf nodes coincide with original graph nodes

After construction, run Dijkstra from each original graph node to solve shortest paths with interval edges in $O((N \log N + M) \log N)$.

πŸ”— Reference Problem: CF786B Legacy (mixed nodeβ†’range, rangeβ†’node, nodeβ†’node edge shortest paths)


Segment Tree Variants Overview

VariantUse CaseComplexity
Basic Segment TreePoint update + range queryO(log N)
Lazy Propagation (range add)Range update + range queryO(log N)
Lazy Propagation (range assign)Range assignment + range queryO(log N)
Dynamic Node AllocationLarge value domain, few operationsO(M log V) space
Weight Segment TreeGlobal K-th smallest, inversionsO(log V) query
Segment Tree Optimized GraphInterval edge shortest pathsO(N log N) construction
Persistent Segment TreeMaintain historical versionsO(log N) per version

⚠️ Common Mistakes

  1. Array size too small: Always allocate tree[4 * MAXN]. For non-power-of-2 array sizes, using 2 * MAXN will overflow.
  2. Wrong identity element for out-of-range: Sum queries return 0; min queries return INT_MAX; max queries return INT_MIN.
  3. Forgetting to update parent node: After updating a child, must recompute parent: tree[node] = tree[2*node] + tree[2*node+1].
  4. 0-indexed vs 1-indexed confusion: This implementation uses 0-indexed arrays but 1-indexed tree nodes; maintain consistency.
  5. Using Segment Tree when prefix sum suffices: If there are no update operations, prefix sum (O(1) query) is better than Segment Tree (O(log N) query). Use simpler tools when appropriate.

Chapter Summary

πŸ“Œ Key Takeaways

OperationTimeKey Code Line
BuildO(N)tree[node] = tree[2*node] + tree[2*node+1]
Point UpdateO(log N)Recurse to leaf, update upward
Range QueryO(log N)Early return when completely inside/outside
SpaceO(4N)Allocate tree[4 * MAXN]

❓ FAQ

Q1: When to choose Segment Tree vs Prefix Sum?

A: Simple rule β€” if the array never changes, prefix sum is better (O(1) query vs O(log N)). If the array is modified (point updates), use Segment Tree or BIT. If you need range updates (add value to a range), use Segment Tree with lazy propagation.

Q2: Why does the tree array need size 4N?

A: Segment Trees are complete binary trees. When N is not a power of 2, the last level may be incomplete but still needs space. Worst case requires about 4N nodes. Using 4*MAXN is a safe upper bound.

Q3: Which is better β€” BIT or Segment Tree?

A: BIT has shorter code (~15 lines vs 30 lines) and smaller constants, but only handles "prefix-decomposable" operations (like sum). Segment Trees are more general (can handle range min/max, GCD, etc.) and support more complex operations (like lazy propagation). In contests: use BIT when possible, switch to Segment Tree when BIT isn't enough.

Q4: What types of queries can Segment Trees handle?

A: Any operation satisfying associativity: sum (+), minimum (min), maximum (max), GCD, XOR, product, etc. The key is having an "identity element" (0 for sum, INT_MAX for min, INT_MIN for max).

Q5: What is lazy propagation? When is it needed?

A: When you need "add V to every element in range [L,R]" (range update), the naive approach updates each leaf from L to R (O(N)), too slow. Lazy propagation "lazily" stores updates at internal nodes, only pushing down to children when they're actually needed for a query, optimizing range updates to O(log N).

πŸ”— Connections to Other Chapters

  • Chapter 3.2 (Prefix Sums): Segment Tree's "simplified version" β€” use prefix sums when there are no updates
  • Chapters 5.1–5.2 (Graphs): Euler Tour + Segment Tree can efficiently handle tree path queries
  • Chapters 6.1–6.3 (DP): Some DP optimizations require Segment Trees to maintain range min/max of DP values
  • Segment Trees are a core data structure at USACO Gold level, mastering them enables solving many Gold problems

Practice Problems

Problem 3.9.1 β€” Classic Range Sum 🟒 Easy Implement a Segment Tree handling N elements and Q queries: point updates or range sums.

Hint Use the complete implementation from Section 3.9.6, with a flag to distinguish query types (1 = update, 2 = query).
βœ… Full Solution
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
long long tree[4*MAXN];
int n, q;

void build(int node, int s, int e, int arr[]) {
    if (s==e) { tree[node]=arr[s]; return; }
    int mid=(s+e)/2;
    build(2*node,s,mid,arr); build(2*node+1,mid+1,e,arr);
    tree[node]=tree[2*node]+tree[2*node+1];
}
void update(int node,int s,int e,int idx,long long val) {
    if (s==e) { tree[node]=val; return; }
    int mid=(s+e)/2;
    if (idx<=mid) update(2*node,s,mid,idx,val);
    else update(2*node+1,mid+1,e,idx,val);
    tree[node]=tree[2*node]+tree[2*node+1];
}
long long query(int node,int s,int e,int l,int r) {
    if (r<s||e<l) return 0;
    if (l<=s&&e<=r) return tree[node];
    int mid=(s+e)/2;
    return query(2*node,s,mid,l,r)+query(2*node+1,mid+1,e,l,r);
}
int arr[MAXN];
int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);
    cin>>n>>q;
    for(int i=1;i<=n;i++) cin>>arr[i];
    build(1,1,n,arr);
    while(q--) {
        int t; cin>>t;
        if(t==1) { int i; long long v; cin>>i>>v; update(1,1,n,i,v); }
        else { int l,r; cin>>l>>r; cout<<query(1,1,n,l,r)<<"\n"; }
    }
}

Complexity: O(N) build, O(log N) per query/update.


Problem 3.9.2 β€” Range Minimum 🟑 Medium Same as above, but query range minimum, with point updates.

Hint Change `+` to `min` in tree operations, return `INT_MAX` for out-of-range.
βœ… Full Solution

Modify two lines in the above solution:

// In build/update:
tree[node] = min(tree[2*node], tree[2*node+1]);
// In query β€” identity element for out-of-range:
if (r < s || e < l) return INT_MAX;
// Merge:
return min(query(2*node,s,mid,l,r), query(2*node+1,mid+1,e,l,r));

Initialization: tree[leaf] = arr[s] (same). The only change is the aggregate function and identity element.


Problem 3.9.3 β€” Inversion Count πŸ”΄ Hard Count pairs (i, j) where i < j and arr[i] > arr[j].

Hint Process elements left to right. For each element x, query how many already-inserted elements are > x.
βœ… Full Solution

Core Idea: Coordinate compress values to [1..N]. For each element x (left to right), inversions += (number of inserted elements) - (number of inserted elements ≀ x) = query(N) - query(x). Then insert x.

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 300005;
int tree[MAXN], n;
void update(int i){for(;i<=n;i+=i&-i) tree[i]++;}
int query(int i){int s=0;for(;i>0;i-=i&-i)s+=tree[i];return s;}

int main(){
    cin>>n;
    vector<int> a(n);
    for(int&x:a)cin>>x;
    // Coordinate compression
    vector<int> sorted=a; sort(sorted.begin(),sorted.end());
    sorted.erase(unique(sorted.begin(),sorted.end()),sorted.end());
    for(int&x:a) x=lower_bound(sorted.begin(),sorted.end(),x)-sorted.begin()+1;
    int m=sorted.size();

    long long inv=0;
    for(int i=0;i<n;i++){
        inv += (i - query(a[i]));  // Count of already-seen elements greater than a[i]
        update(a[i]);
    }
    cout<<inv<<"\n";
}

Complexity: O(N log N), using BIT (more appropriate than Segment Tree for this problem).


πŸ† Challenge: USACO 2016 February Gold: Fencing the Cows A problem requiring range maximum queries with updates. Try solving it with both BIT and Segment Tree to understand the tradeoffs.