CO1 Topic 6 of 34
Segment trees and range trees
Trees that answer questions about ranges: a segment tree gives the sum (or min, or max) of any interval of an array in O(log n), even while the array changes; a range tree counts the points inside any rectangle.
You will be able to
- Build a segment tree and answer range-sum and range-minimum queries
- Update an element and repair the tree in O(log n)
- Explain how a 2-D range tree answers rectangle queries in O(log² n)
- Choose between prefix sums, segment trees and range trees for a problem
Read
- Textbook 1, §14.3 Interval trees (augmenting data structures)
- Reference book 1, §15.4 (range search)
- Reference book 2, §11 (balanced search trees and their uses)
Watch
Intuition
You keep daily sales for a year in an array and are asked, thousands of times, "what were the total sales from day l to day r?" — while some days' figures keep getting corrected.
- Adding up a[l..r] each time costs O(n) per query.
- A prefix-sum array answers in O(1), but one correction costs O(n) to rebuild.
- A segment tree answers both in O(log n).
Each node stores the sum of a segment of the array; the root covers everything, its children cover the two halves, and so on down to single elements.
digraph {
rankdir=TB; node [shape=box, style=rounded, fontname="Inter"]; edge [dir=none];
r [label="[0..5]\nsum 36"];
a [label="[0..2]\nsum 9"]; b [label="[3..5]\nsum 27"];
a1 [label="[0..1]\n4"]; a2 [label="[2]\n5"]; b1 [label="[3..4]\n16"]; b2 [label="[5]\n11"];
l0 [label="[0]\n1"]; l1 [label="[1]\n3"]; l3 [label="[3]\n7"]; l4 [label="[4]\n9"];
r -> a; r -> b; a -> a1; a -> a2; b -> b1; b -> b2; a1 -> l0; a1 -> l1; b1 -> l3; b1 -> l4;
}
Segment tree for a = [1, 3, 5, 7, 9, 11]. The query sum(1..4) = 3 + 5 + 7 + 9 = 24 is assembled from just three nodes, [1], [2] and [3..4]: 3 + 5 + 16.
How it works
Query(l, r) at a node covering [lo..hi]:
- no overlap with [l..r] → contribute 0;
- completely inside [l..r] → contribute the stored value, don't go further;
- partial overlap → ask both children and add.
At most about 4 nodes per level are visited, so a query costs O(log n).
Update(i, value): change the leaf for position i, then recompute the sums on the path back up to the root — O(log n).
The same structure works for any associative operation: min, max, gcd, product. Only the combine step changes.
Range trees (2-D range counting). Given n points (x, y), count how many lie in a rectangle [x₁, x₂] × [y₁, y₂].
- Build a balanced tree on the x-coordinates (like a segment tree over the points sorted by x).
- At every node, also store the y-coordinates of all points in its subtree, sorted.
- A query splits [x₁, x₂] into O(log n) canonical nodes; in each, binary-search the sorted y-list to count points with y₁ ≤ y ≤ y₂.
That gives O(log² n) per query using O(n log n) space (each point appears in one y-list per level).
Python implementation
from bisect import bisect_left, bisect_right
class SegmentTree:
"""Range sums with point updates. tree[1] is the root; node i has children 2i and 2i+1."""
def __init__(self, a):
self.n = len(a)
self.tree = [0] * (4 * self.n)
self._build(a, 1, 0, self.n - 1)
def _build(self, a, node, lo, hi):
if lo == hi:
self.tree[node] = a[lo]
return
mid = (lo + hi) // 2
self._build(a, 2 * node, lo, mid)
self._build(a, 2 * node + 1, mid + 1, hi)
self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]
def query(self, l, r, node=1, lo=0, hi=None):
hi = self.n - 1 if hi is None else hi
if r < lo or hi < l:
return 0 # no overlap
if l <= lo and hi <= r:
return self.tree[node] # fully inside
mid = (lo + hi) // 2
return self.query(l, r, 2 * node, lo, mid) + self.query(l, r, 2 * node + 1, mid + 1, hi)
def update(self, i, value, node=1, lo=0, hi=None):
hi = self.n - 1 if hi is None else hi
if lo == hi:
self.tree[node] = value
return
mid = (lo + hi) // 2
if i <= mid:
self.update(i, value, 2 * node, lo, mid)
else:
self.update(i, value, 2 * node + 1, mid + 1, hi)
self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]
class RangeTree2D:
"""Static 2-D range counting: a tree over x, each node keeping its points' y-values sorted."""
def __init__(self, points):
self.pts = sorted(points)
self.xs = [p[0] for p in self.pts]
self.n = len(self.pts)
self.ys = [[] for _ in range(4 * self.n)]
self._build(1, 0, self.n - 1)
def _build(self, node, lo, hi):
if lo == hi:
self.ys[node] = [self.pts[lo][1]]
return
mid = (lo + hi) // 2
self._build(2 * node, lo, mid)
self._build(2 * node + 1, mid + 1, hi)
self.ys[node] = sorted(self.ys[2 * node] + self.ys[2 * node + 1])
def count(self, x1, x2, y1, y2):
l, r = bisect_left(self.xs, x1), bisect_right(self.xs, x2) - 1
return self._count(1, 0, self.n - 1, l, r, y1, y2) if l <= r else 0
def _count(self, node, lo, hi, l, r, y1, y2):
if r < lo or hi < l:
return 0
if l <= lo and hi <= r: # canonical node: binary search on y
ys = self.ys[node]
return bisect_right(ys, y2) - bisect_left(ys, y1)
mid = (lo + hi) // 2
return (self._count(2 * node, lo, mid, l, r, y1, y2) +
self._count(2 * node + 1, mid + 1, hi, l, r, y1, y2))
if __name__ == "__main__":
st = SegmentTree([1, 3, 5, 7, 9, 11])
print(st.query(1, 4), st.query(0, 5)) # 24 36
st.update(2, 10) # a[2]: 5 -> 10
print(st.query(1, 4)) # 29
rt = RangeTree2D([(1, 5), (2, 2), (3, 8), (5, 4), (6, 7), (8, 1)])
print(rt.count(2, 6, 2, 7)) # 3
Complexity
| Structure | Build | Query | Update | Space |
|---|---|---|---|---|
| prefix sums | O(n) | O(1) | O(n) | O(n) |
| segment tree | O(n) | O(log n) | O(log n) | O(n) (array of size 4n) |
| 2-D range tree (as above) | O(n log n) | O(log² n) | static | O(n log n) |
Common mistakes
- Array too small. A segment tree stored in an array needs up to 4n slots, not 2n, when n is not a power of two.
- Mixing inclusive and exclusive ends. Decide whether r is included and stay consistent in query and update.
- Using a segment tree for a non-associative operation such as "average" directly — store sum and count instead, and divide at the end.
- Range trees: searching y in every node. Only the O(log n) canonical nodes need a binary search.
Problems
- For a = [2, 4, 6, 8, 10, 12, 14], draw the segment tree of sums and list the nodes used to answer sum(2..5).
- Lab: change
SegmentTreeto answer range minimum queries. What should "no overlap" return? - Lab: using a segment tree, answer: "how many elements in a[l..r] are greater than k?" for many queries on a fixed array. (Hint: store sorted lists, as in the range tree.)
- For the points (1, 5), (2, 2), (3, 8), (5, 4), (6, 7), (8, 1), count by hand the points in [2, 6] × [2, 7] and check the program's answer.
- Why is a segment tree with lazy propagation needed when updates change a whole range at once? Sketch the idea.
Quiz
GATE-style practice
Questions in the GATE CSE formats: MCQ (one correct option), MSQ (one or more correct options) and NAT (type a number). Each comes with a full worked solution. These are original practice questions, not past papers; for the real thing, see the official GATE site and its previous question papers.