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

Introduction to Segment Trees

Segment trees are one of the most powerful data structures in competitive programming and systems design. They solve a deceptively simple problem, answering range queries efficiently while also supporting updates, but the idea behind them is elegant and worth understanding deeply.

This post builds the foundation. We'll understand why segment trees exist, what problem they solve that simpler approaches can't, and how the tree structure maps to the array. By the end, you'll be able to draw a segment tree from any array and understand exactly what each node represents.

The Range Query Problem

Imagine you have an array of numbers:

arr = [1, 3, 5, 7, 9, 11, 2, 4]

And you need to repeatedly answer questions like:

These are range queries: given two indices l and r, compute some aggregate function over the sub-array arr[l..r].

The Naive Approach

Loop from l to r, accumulate the result. Each query takes O(n) in the worst case (when the range spans the entire array). Updates are instant, just change arr[i] in O(1).

If you have Q queries on an array of size n, the total time is O(Q · n). For n = 105 and Q = 105, that's 1010 operations, way too slow.

Prefix Sums

Precompute a prefix sum array: prefix[i] = arr[0] + arr[1] + ... + arr[i]. Then sum(l, r) = prefix[r] - prefix[l-1] in O(1).

But there's a catch: if an element changes (arr[i] = newVal), you must rebuild all of prefix[i..n-1], that's O(n) per update. If updates are frequent, prefix sums fall apart.

The Core Trade-off

ApproachBuildQueryUpdateBest when
Brute forceO(1)O(n)O(1)Very few queries
Prefix sumsO(n)O(1)O(n)No updates (static)
Sqrt decompositionO(n)O(√n)O(√n)Simpler to code
Segment treeO(n)O(log n)O(log n)Queries + updates both frequent

Segment trees hit the sweet spot: both queries and updates are O(log n). This makes them ideal when you have a mix of thousands of queries and updates.

The key insight: We want something between "store nothing extra" (brute force) and "precompute everything" (prefix sums). Segment trees precompute partial aggregates, sums of power-of-two-sized blocks, so that any range query can be answered by combining at most O(log n) precomputed values.

The Structure: Divide and Conquer on Ranges

The idea is beautifully simple: recursively split the array in half and store the aggregate of each half.

Start with the full array [0, n-1]. Split it into [0, mid] and [mid+1, n-1] where mid = (0 + n-1) / 2. Recursively split each half until you reach individual elements. Store the aggregate (e.g., sum) at each level.

Building from the Array

Let's build a segment tree for arr = [1, 3, 5, 7, 9, 11, 2, 4]:

  1. Level 3 (leaves): Each leaf holds one element. arr[0]=1, arr[1]=3, arr[2]=5, ..., arr[7]=4.
  2. Level 2: Combine pairs. [0-1]=1+3=4, [2-3]=5+7=12, [4-5]=9+11=20, [6-7]=2+4=6.
  3. Level 1: Combine again. [0-3]=4+12=16, [4-7]=20+6=26.
  4. Level 0 (root): [0-7]=16+26=42. This is the sum of the entire array.
42[0-7] 16[0-3] 26[4-7] 4[0-1] 12[2-3] 20[4-5] 6[6-7] 1[0] 3[1] 5[2] 7[3] 9[4] 11[5] 2[6] 4[7]
Segment tree for arr = [1, 3, 5, 7, 9, 11, 2, 4]. Each internal node stores the sum of its range. The bracket labels show which array indices the node covers.

What Each Node Represents

Every node in the tree represents a contiguous sub-range of the original array:

This means: to find the sum of any range [l, r], we don't need to look at every element. We just need to find nodes whose ranges together cover [l, r] exactly, and there are at most O(log n) such nodes.

Why O(log n)? The Intuition

Why queries are fast

Consider querying sum(2, 5) on our example tree. We start at the root [0-7]. Its range isn't fully inside [2, 5], so we go to both children. The left child [0-3] partially overlaps, we recurse. The right child [4-7] partially overlaps, we recurse there too.

But here's the key: at each level of the tree, at most 2 nodes are "partially overlapping" (one on the left boundary, one on the right boundary). All other nodes are either fully inside (we take their value immediately) or fully outside (we skip them). Since the tree has O(log n) levels, we visit at most O(log n) nodes.

Think of it this way: The query range [l, r] can "straddle" at most one split at each level. At the boundary, it may need to recurse into both children. But in the interior, entire subtrees are either fully contained (instant answer) or fully excluded (pruned). So the total work is proportional to the tree height = O(log n).

Why updates are fast

When arr[i] changes, only nodes whose range contains index i need updating. Starting from the root, there's exactly one path down to the leaf at index i, every other node's range either doesn't contain i (skip) or is on this single path. The path has length O(log n), so only O(log n) nodes need recalculating.

Storing the Tree in an Array

We don't use pointer-based nodes for segment trees. Instead, we use an array with 1-based indexing, exactly like a binary heap:

Why Does This Formula Work?

Write each node’s index in binary. The pattern is immediate:

That’s literally what ×2 and ×2+1 do in binary: shift left and append a bit. So every node’s index is 1 followed by its path from the root, where 0 = left and 1 = right:

IndexBinaryPath from Root
11(root)
210root → left
311root → right
5101root → leftright
101010root → leftrightleft
121100root → rightleftleft

Going to a child = appending one more bit to the path. Multiplying by 2 appends 0 (left child), adding 1 flips that last bit to 1 (right child). The formula is binary arithmetic on tree paths. That’s all there is to it.

Parent works the same way in reverse: Removing the last bit = dividing by 2 = going to the parent. Node 12 (1100) → parent = 6 (110) → parent = 3 (11) → parent = 1 (1 = root). Integer division i / 2 strips the last bit.

▶ Watch BFS Numbering Build the 2i Formula

Step through to see how level-order numbering naturally produces the 2i / 2i+1 relationship. Watch the binary encoding in action.

Press Next → to start building the tree in BFS order.

How much space do we need?

For an array of size n, we store the segment tree in a flat array. But how big must that array be? The answer is 4n. Here's the step-by-step reasoning:

Case 1: n is a power of 2

When n = 2k, the tree is a perfect binary tree. Every level is completely filled.

The total node count is the sum of every level: 1 + 2 + 4 + … + n. This is a geometric progression (each term is double the previous one), so we can use the standard GP sum formula.

The GP sum, step by step
A geometric series with first term a, ratio r, and m terms sums to:
  a · (rm − 1) / (r − 1)

Here the terms are 1, 2, 4, …, 2k (that is n = 2k), so:
  a = 1,   r = 2,   m = k + 1 terms (levels 0 through k)

Sum = 1 · (2k+1 − 1) / (2 − 1) = 2k+1 − 1
     = 2 · 2k − 1 = 2n − 1

Two things make this click. First, there are k + 1 levels, not k: the root is level 0 and the leaves are level k, so counting both endpoints gives k + 1 terms. Second, the intuition behind 1 + 2 + 4 + … + 2k = 2k+1 − 1 is that doubling and adding one always reaches the next power of two: 1 + 1 = 2, 2 + 2 = 4, and in general the sum of all the smaller powers is exactly one less than the next power. In binary it is just k + 1 ones (e.g. 1111₂ = 15 = 24 − 1).

So total nodes = 2n − 1. Since we use 1-based indexing (root at index 1), the largest index is 2n − 1 and we need an array of size 2n. Clean and tight.

Example: n = 8 (23)
Nodes: 2×8 − 1 = 15
Array indices used: 1 through 15
Array size needed: 16 (indices 0–15, with 0 unused)

Case 2: n is NOT a power of 2 — the tricky case

When n isn't a power of 2, the tree is not perfectly balanced. Some branches go one level deeper than others. The problem: a node at a deep level might have a high array index even though there are few total nodes.

Example: n = 5
Next power of 2: 8 → tree depth = ⌈log2(5)⌉ + 1 = 4 levels
Deepest leaf can be at level 3, where indices go up to 24 − 1 = 15
But the total number of actual nodes is only 9
If you allocate 2n = 10, indices like 12 or 13 would overflow!

Here's what happens visually. Node 1 splits [0–4] into [0–2] and [3–4]. The left subtree [0–2] splits into [0–1] and [2], and [0–1] splits again into leaves [0] and [1]. That path is 3 edges deep, making the leaves land at indices 8 and 9 — well beyond 2n = 10's safe range for child computations at index 5 (children would be 10 and 11).

The safe formula: allocate 4n

Here is the clean argument for why 4n always works. The build recursion keeps splitting a range in half until every leaf covers a single element, so the tree has depth d = ⌈log2(n)⌉ (root at depth 0). With 1-based heap indexing a node at index i has children 2i and 2i+1, so the index roughly doubles every time we descend one level.

The clean way to bound the largest index is to round n up to the next power of two, call it N = 2⌈log2(n)⌉. A segment tree drawn over N leaves is a perfect binary tree with 2N − 1 nodes, and its deepest leaves occupy heap indices N through 2N − 1. Any real segment tree over n ≤ N elements is a subset of that shape, so the largest index it can ever touch is at most 2N − 1.

The two-line bound
N is the smallest power of two with N ≥ n, so N < 2n.
Largest index used = 2N − 1 < 2(2n) − 1 = 4n − 1 < 4n.

That is the whole proof: every index the tree can possibly use is strictly less than 4n, so an array of size 4n can never overflow, for any n. Note the space is still O(n) asymptotically; the 4 is just the constant factor you must actually allocate to stay safe.

vector<int> tree(4 * n);  // always safe, never overflows
nPower of 2?Actual nodesMax index used4n
47716
591520
8151532
9173136
100199255400
TL;DR: When n is a power of 2, 2n suffices. When it isn't, the tree depth rounds up, pushing some leaf indices much higher than 2n. Allocating 4n covers the worst case for any n. Some people use 2 × nextPowerOf2(n) for a tighter bound, but 4n is simpler and universally safe. Never use 2n unless you've verified n is a power of 2.

Here's the mapping for our 8-element array (which is a power of 2, so the tree is perfect):

Tree IndexRangeValue (sum)Left ChildRight Child
1[0-7]4223
2[0-3]1645
3[4-7]2667
4[0-1]489
5[2-3]121011
6[4-5]201213
7[6-7]61415
8[0]1leaf
9[1]3leaf
10[2]5leaf
11[3]7leaf
12[4]9leaf
13[5]11leaf
14[6]2leaf
15[7]4leaf

▶ Interactive Segment Tree Explorer

Click any node to see what range it covers and its value. The corresponding array elements highlight below.

Array

Beyond Sums: Any Associative Operation

Segment trees aren't limited to sums. They work for any associative operation, one where merge(merge(a, b), c) = merge(a, merge(b, c)):

OperationIdentityMergeExample Use
Sum0a + bRange sum queries
Min+∞min(a, b)Range minimum query
Max-∞max(a, b)Range maximum query
GCD0gcd(a, b)Range GCD
XOR0a ^ bRange XOR
Product1a * bRange product (mod)
Count0a + bCount of elements > k

The tree structure stays exactly the same. Only the merge function and identity element change. The rest of this series uses sum as the running example, but everything generalizes.

When to Use a Segment Tree

Use a segment tree when:

Don't use a segment tree when:

Segment Tree vs. Other Structures

StructureRange QueryPoint UpdateRange UpdateCode Complexity
Prefix sumsO(1)O(n)O(n)Very simple
Sqrt decompositionO(√n)O(1)O(√n)Simple
Fenwick / BITO(log n)O(log n)-Moderate
Segment treeO(log n)O(log n)O(log n)*Moderate
Balanced BSTO(log n)O(log n)O(log n)Complex

* Range update with lazy propagation (covered in a later post).

What's Coming Next

This series covers segment trees from the ground up:

  1. This post: Why segment trees exist, the structure, array representation.
  2. Building & Querying: How the build algorithm works, how range queries decompose into O(log n) nodes, with step-by-step animations.
  3. Updates: Point updates, how changes propagate up, and the transition to range updates.
  4. Lazy Propagation: The key technique that makes range updates O(log n). Explained in extreme detail with animations.
  5. Patterns & Practice: Common segment tree problems, variations (min/max/GCD), and interview-ready templates.

Summary