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
Awith 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 Structure | Build | Query | Update | Best For |
|---|---|---|---|---|
| Plain Array | O(N) | O(N) | O(1) | Updates only |
| Prefix Sum | O(N) | O(1) | O(N) | Queries only |
| Segment Tree | O(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]):
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:
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:
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)
| Feature | Segment Tree | Fenwick Tree (BIT) |
|---|---|---|
| Code Complexity | Medium (~30 lines) | Simple (~15 lines) |
| Range Query | Any associative operation | Prefix sums only |
| Range Update | Yes (with lazy propagation) | Yes (with tricks) |
| Point Update | O(log N) | O(log N) |
| Space | O(4N) | O(N) |
| Use Case | Range min/max, complex queries | Prefix 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.
π‘ 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:
- Apply the lazy update to this node's value
- Pass the lazy update to both children (push down)
- 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:
- Forgetting
pushDownbefore recursing β children will receive their own lazy on top of parent's, causing wrong query results - Wrong size multiplier β writing
tree[node] += valinstead oftree[node] += val * (end - start + 1). The node stores a sum, and adding val to(end-start+1)elements means the sum increases byval Γ size - Not initializing
lazy[]to 0 β usememset(lazy, 0, sizeof(lazy))or initialize inbuild() - 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
ktonelements increases sum byk*n)
| Update | Query | Lazy Stores | pushDown Formula |
|---|---|---|---|
| Range add | Range sum | Add delta | tree[child] += lazy * size; lazy[child] += lazy |
| Range assign | Range sum | Assign value | tree[child] = lazy * size; lazy[child] = lazy |
| Range add | Range min | Add delta | tree[child] += lazy; lazy[child] += lazy |
| Range assign | Range min | Assign value | tree[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:
| Tree | Direction | Description |
|---|---|---|
| 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
| Variant | Use Case | Complexity |
|---|---|---|
| Basic Segment Tree | Point update + range query | O(log N) |
| Lazy Propagation (range add) | Range update + range query | O(log N) |
| Lazy Propagation (range assign) | Range assignment + range query | O(log N) |
| Dynamic Node Allocation | Large value domain, few operations | O(M log V) space |
| Weight Segment Tree | Global K-th smallest, inversions | O(log V) query |
| Segment Tree Optimized Graph | Interval edge shortest paths | O(N log N) construction |
| Persistent Segment Tree | Maintain historical versions | O(log N) per version |
β οΈ Common Mistakes
- Array size too small: Always allocate
tree[4 * MAXN]. For non-power-of-2 array sizes, using2 * MAXNwill overflow. - Wrong identity element for out-of-range: Sum queries return 0; min queries return
INT_MAX; max queries returnINT_MIN. - Forgetting to update parent node: After updating a child, must recompute parent:
tree[node] = tree[2*node] + tree[2*node+1]. - 0-indexed vs 1-indexed confusion: This implementation uses 0-indexed arrays but 1-indexed tree nodes; maintain consistency.
- 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
| Operation | Time | Key Code Line |
|---|---|---|
| Build | O(N) | tree[node] = tree[2*node] + tree[2*node+1] |
| Point Update | O(log N) | Recurse to leaf, update upward |
| Range Query | O(log N) | Early return when completely inside/outside |
| Space | O(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 vsO(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*MAXNis 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_MAXfor min,INT_MINfor 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 toO(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.