Skip to main content

Segment Tree

A Segment Tree is an advanced data structure that allows you to efficiently perform range queries and point updates on an array — operations that would take O(n) per query with brute force, reduced to O(log n) with a Segment Tree.


🧠 Why Segment Tree?

Problem: Given array [1, 3, 5, 7, 9, 11]
Answer Q queries: "What is the sum of elements from index L to R?"
Also handle U updates: "Change element at index i to value v"

❌ BRUTE FORCE:
Query: O(n) per query → 100 queries on 10⁶ array = 10⁸ operations (TLE)
Update: O(1)

✅ SEGMENT TREE:
Build: O(n)
Query: O(log n)
Update: O(log n)

🌳 How the Tree is Built

A Segment Tree maps an array into a binary tree where:

  • Each leaf node stores one element
  • Each internal node stores the result (sum/min/max) of its children's range
Array: [1, 3, 5, 7, 9, 11]
Index: 0 1 2 3 4 5

[0,5] = 36
/ \
[0,2] = 9 [3,5] = 27
/ \ / \
[0,1]=4 [2,2]=5 [3,4]=16 [5,5]=11
/ \ / \
[0,0]=1 [1,1]=3 [3,3]=7 [4,4]=9

Node stores: (range, value)

Array Representation of Tree

For array of size n, segment tree uses array of size 4*n

tree[1] = root = sum of entire array
tree[2] = left child (indices 0 to mid)
tree[3] = right child (indices mid+1 to end)
tree[4] = left child of tree[2]
tree[5] = right child of tree[2]
...and so on

For node at index i:
Left child → 2*i
Right child → 2*i + 1
Parent → i/2

🔨 Operation 1: Build

Visual Step-by-Step

Array: [1, 3, 5, 7, 9, 11]

Step 1: Fill leaf nodes
tree[8]=1, tree[9]=3, tree[5]=5, tree[10]=7, tree[11]=9, tree[7]=11

Step 2: Fill internal nodes bottom-up
tree[4] = tree[8]+tree[9] = 1+3 = 4
tree[6] = tree[10]+tree[11] = 7+9 = 16
tree[2] = tree[4]+tree[5] = 4+5 = 9
tree[3] = tree[6]+tree[7] = 16+11 = 27
tree[1] = tree[2]+tree[3] = 9+27 = 36

Correct tree (1-indexed, nodes store range sum):
tree[1] = 36 (range [0,5])
tree[2] = 9 (range [0,2])
tree[3] = 27 (range [3,5])
tree[4] = 4 (range [0,1])
tree[5] = 5 (range [2,2])
tree[6] = 16 (range [3,4])
tree[7] = 11 (range [5,5])
tree[8] = 1 (range [0,0])
tree[9] = 3 (range [1,1])
tree[10] = 7 (range [3,3])
tree[11] = 9 (range [4,4])

Code

class SegmentTree:
def __init__(self, arr):
self.n = len(arr)
self.tree = [0] * (4 * self.n)
self.build(arr, 0, self.n - 1, 1)

def build(self, arr, start, end, node):
if start == end:
# Leaf node — store the element
self.tree[node] = arr[start]
else:
mid = (start + end) // 2
left_child = 2 * node
right_child = 2 * node + 1

# Recursively build left and right subtrees
self.build(arr, start, mid, left_child)
self.build(arr, mid + 1, end, right_child)

# Internal node stores sum of children
self.tree[node] = self.tree[left_child] + self.tree[right_child]

Time Complexity: O(n)  |  Space Complexity: O(n)


🔍 Operation 2: Range Query

Visual Dry-Run

Array: [1, 3, 5, 7, 9, 11]   Query: sum(L=1, R=4)  Expected: 3+5+7+9 = 24

Start at root node [0,5]:
Query [1,4] overlaps [0,5] → go to both children

Left child [0,2]:
Query [1,4] overlaps [0,2] → go to both children

Left child [0,1]:
Query [1,4] overlaps [0,1] → go to both children

Left child [0,0]:
Query [1,4] does NOT overlap [0,0] → return 0 ❌

Right child [1,1]:
Query [1,4] FULLY covers [1,1] → return tree value = 3 ✅

Right child [2,2]:
Query [1,4] FULLY covers [2,2] → return tree value = 5 ✅

Right child [3,5]:
Query [1,4] overlaps [3,5] → go to both children

Left child [3,4]:
Query [1,4] FULLY covers [3,4] → return tree value = 16 ✅

Right child [5,5]:
Query [1,4] does NOT overlap [5,5] → return 0 ❌

Final Answer: 0 + 3 + 5 + 16 + 0 = 24 ✅

3 Cases in Query

1. NO OVERLAP    → current range is completely outside query → return 0
2. FULL OVERLAP → current range is completely inside query → return tree[node]
3. PARTIAL → current range partially overlaps query → recurse on children

Code

    def query(self, l, r, start, end, node):
# Case 1: No overlap
if r < start or end < l:
return 0

# Case 2: Full overlap
if l <= start and end <= r:
return self.tree[node]

# Case 3: Partial overlap — recurse on both children
mid = (start + end) // 2
left_sum = self.query(l, r, start, mid, 2 * node)
right_sum = self.query(l, r, mid + 1, end, 2 * node + 1)
return left_sum + right_sum

# Usage: st.query(1, 4, 0, n-1, 1) → sum from index 1 to 4

Time Complexity: O(log n)  |  Space Complexity: O(log n) recursion stack


✏️ Operation 3: Point Update

Visual Dry-Run

Array: [1, 3, 5, 7, 9, 11]   Update: index=2, newValue=10
(Changing 5 → 10, so diff = +5)

Start at root [0,5]:
index=2 is in [0,5] → go to both children

Left child [0,2]:
index=2 is in [0,2] → go to right child only

Right child [2,2]:
index=2 == [2,2] → LEAF NODE
tree[node] = 10 ✅ (update leaf)

Back at [0,2]: tree[node] = tree[left] + tree[right] = 4 + 10 = 14 ✅
Back at [0,5]: tree[node] = 14 + 27 = 41 ✅

All ancestor nodes updated automatically!

Code

    def update(self, idx, new_val, start, end, node):
if start == end:
# Leaf node — update value
self.tree[node] = new_val
else:
mid = (start + end) // 2
if idx <= mid:
# Index is in left subtree
self.update(idx, new_val, start, mid, 2 * node)
else:
# Index is in right subtree
self.update(idx, new_val, mid + 1, end, 2 * node + 1)

# Update current node after child is updated
self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

# Usage: st.update(2, 10, 0, n-1, 1) → change index 2 to value 10

Time Complexity: O(log n)  |  Space Complexity: O(log n) recursion stack


⚡ Advanced: Lazy Propagation (Range Update)

The Problem Without Lazy

Without lazy: "Add 5 to all elements from index 1 to 4"
→ We'd call point update 4 times → O(4 log n)

With lazy propagation:
→ Mark the range as "pending update" and defer it → O(log n)

Core Idea

Keep a separate lazy[] array.
lazy[node] = pending value to be added to all elements in this node's range.

When we visit a node:
1. Apply lazy[node] to tree[node] first
2. Pass lazy[node] down to children (propagate)
3. Clear lazy[node]

This way, we defer work until we actually need it.

Code

class SegmentTreeLazy:
def __init__(self, arr):
self.n = len(arr)
self.tree = [0] * (4 * self.n)
self.lazy = [0] * (4 * self.n) # pending updates
self.build(arr, 0, self.n - 1, 1)

def build(self, arr, start, end, node):
if start == end:
self.tree[node] = arr[start]
else:
mid = (start + end) // 2
self.build(arr, start, mid, 2 * node)
self.build(arr, mid + 1, end, 2 * node + 1)
self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

def propagate(self, node, start, end):
if self.lazy[node] != 0:
mid = (start + end) // 2
# Apply pending update to children
self.tree[2 * node] += self.lazy[node] * (mid - start + 1)
self.tree[2 * node + 1] += self.lazy[node] * (end - mid)
# Pass lazy to children
self.lazy[2 * node] += self.lazy[node]
self.lazy[2 * node + 1] += self.lazy[node]
# Clear current lazy
self.lazy[node] = 0

def range_update(self, l, r, val, start, end, node):
if r < start or end < l:
return
if l <= start and end <= r:
# Full overlap — update and mark lazy
self.tree[node] += val * (end - start + 1)
self.lazy[node] += val
return
# Partial overlap — propagate first then recurse
self.propagate(node, start, end)
mid = (start + end) // 2
self.range_update(l, r, val, start, mid, 2 * node)
self.range_update(l, r, val, mid + 1, end, 2 * node + 1)
self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

def query(self, l, r, start, end, node):
if r < start or end < l:
return 0
if l <= start and end <= r:
return self.tree[node]
# Propagate before querying children
self.propagate(node, start, end)
mid = (start + end) // 2
return (self.query(l, r, start, mid, 2 * node) +
self.query(l, r, mid + 1, end, 2 * node + 1))

Time Complexity: O(log n) per range update and query  |  Space Complexity: O(n)


📊 Complexity Summary

OperationWithout Segment TreeWith Segment TreeWith Lazy Propagation
BuildO(1)O(n)O(n)
Range QueryO(n)O(log n)O(log n)
Point UpdateO(1)O(log n)O(log n)
Range UpdateO(n)O(n log n)O(log n)

❌ Common Mistakes

  1. Tree array too small — Always allocate 4 * n size for the tree array, not 2 * n.
  2. Starting node at 0 — Segment trees use 1-indexed nodes. Left child = 2*i, right = 2*i+1 only works when root is at index 1.
  3. Forgetting to propagate lazy — In lazy propagation, always call propagate() before recursing into children during both query and update.
  4. Integer overflow — For large arrays with large values, use long long (C++) or long (Java) for the tree array.
  5. Wrong base case in query — No-overlap must return the identity element (0 for sum, INT_MAX for min, INT_MIN for max) not -1.

🏋️ Practice Problems

#ProblemOperationDifficulty
1Range Sum Query — MutablePoint Update + Range Query🟡 Medium
2Count of Smaller Numbers After SelfRange Query🔴 Hard
3Range Minimum QueryRange Query🟡 Medium
4Interval Sum (Range Update)Lazy Propagation🟡 Medium
5Number of Longest Increasing SubsequenceRange Query🔴 Hard
6My Calendar IIIRange Update + Range Query🔴 Hard
7Rectangle Area IICoordinate Compression + Segment Tree🔴 Hard

🌍 Real-World Applications

  • Database indexing — Range aggregate queries (SUM, MIN, MAX) over rows
  • Competitive programming — Most Codeforces Div. 1 problems involving range queries
  • Geographic Information Systems (GIS) — Range queries on spatial data
  • Network routing — Finding minimum latency path in a range

🔗 References