DEV Community

Timevolt
Timevolt

Posted on

Segment Trees: The One Ring to Rule Range Queries

The Quest Begins (The “Why”)

Ever felt like you’re stuck grinding through a massive array, trying to answer “what’s the sum between indices L and R?” over and over again? I was there last month, prepping for a backend interview, when the interviewer tossed a classic: “Given an array that can change, return the sum of any sub‑array in log time.” My first instinct? Loop from L to R each query—O(n) per call. It worked for tiny test cases, but the moment the array hit 10⁵ elements and the queries piled up, my solution choked like a boss with too many hit points.

I spent three hours debugging that brute‑force loop, watching my runtime creep up, and honestly felt like Frodo staring at Mount Doom with a plastic spoon. I knew there had to be a smarter way—something that could preprocess the data once and then answer each range query quickly, even when the array mutates. That’s when the segment tree whispered its promise: O(log n) query and update, O(n) build.

The Revelation (The Insight)

So why does a segment tree work? Think of the array as a line of soldiers. Instead of asking each soldier individually for their strength, we build a hierarchy: each node stores the aggregated strength of a contiguous segment. The root covers the whole line; its two children split it in half, and so on, down to leaves that represent single elements.

The magic lies in overlap decomposition. Any interval [L,R] can be expressed as a disjoint union of O(log n) tree nodes—exactly the nodes whose segments fully lie inside the query range and whose parents stick out. Because we already stored the sum (or min, max, etc.) for each node, we just add up those O(log n) pre‑computed values. No need to look at every element; we reuse work done during the build.

When an element changes, we only need to update the nodes on the path from that leaf to the root—again O(log n). The rest of the tree stays valid because their stored aggregates are recomputed from their children.

It felt like discovering the secret level in Super Mario where a hidden block gives you infinite lives: the same structure that lets you answer a query also lets you keep it fresh with updates.

Wielding the Power (Code & Examples)

Building the Tree

We’ll implement a classic range‑sum segment tree with point updates. The tree is stored in an array tree of size 4*n (enough for a full binary tree).

class SegTree:
    def __init__(self, data):
        self.n = len(data)
        self.tree = [0] * (4 * self.n)
        self._build(data, 1, 0, self.n - 1)

    def _build(self, data, node, l, r):
        if l == r:                     # leaf
            self.tree[node] = data[l]
            return
        mid = (l + r) // 2
        self._build(data, node * 2, l, mid)          # left child
        self._build(data, node * 2 + 1, mid + 1, r)  # right child
        self.tree[node] = self.tree[node * 2] + self.tree[node * 2 + 1]
Enter fullscreen mode Exit fullscreen mode

Why O(n) build?

Each node is visited exactly once, and the work per node is O(1). With roughly 2n nodes in a full binary tree, the total is linear.

Query – Sum on [L,R]

    def query(self, ql, qr):
        return self._query(1, 0, self.n - 1, ql, qr)

    def _query(self, node, l, r, ql, qr):
        # no overlap
        if ql > r or qr < l:
            return 0
        # total overlap
        if ql <= l and r <= qr:
            return self.tree[node]
        # partial overlap
        mid = (l + r) // 2
        left_sum = self._query(node * 2, l, mid, ql, qr)
        right_sum = self._query(node * 2 + 1, mid + 1, r, ql, qr)
        return left_sum + right_sum
Enter fullscreen mode Exit fullscreen mode

The recursion descends only into nodes that intersect the query range. At each level we either return a stored sum (total overlap) or skip the node (no overlap). Because the tree height is log₂ n, we visit at most 2·log₂n nodes → O(log n).

Update – Point Assign

    def update(self, idx, value):
        self._update(1, 0, self.n - 1, idx, value)

    def _update(self, node, l, r, idx, value):
        if l == r:                     # leaf
            self.tree[node] = value
            return
        mid = (l + r) // 2
        if idx <= mid:
            self._update(node * 2, l, mid, idx, value)
        else:
            self._update(node * 2 + 1, mid + 1, r, idx, value)
        self.tree[node] = self.tree[node * 2] + self.tree[node * 2 + 1]
Enter fullscreen mode Exit fullscreen mode

Again we walk from root to leaf, updating O(log n) nodes.

Common Traps

Trap What Happens Fix
Off‑by‑one in query bounds Accidentally excludes qr or includes an extra element → wrong sum. Keep the interval inclusive on both ends (ql ≤ idx ≤ qr) and check if ql > r or qr < l for no overlap.
Using the wrong size for tree Allocating 2*n works only for a perfect binary tree; for non‑power‑of‑two n you’ll overwrite. Safe size is 4*n (or compute 1 << (ceil_pow2(n)+1)).
Forgetting to recompute parent after child update Stale aggregates propagate upward, breaking future queries. After updating a child, always set tree[node] = tree[left] + tree[right].

Interview‑Style Problems

  1. Range Sum with Updates – Classic LeetCode “Range Sum Query – Mutable”. The segment tree above solves it in O(log n) per operation.
  2. Count of Numbers Greater Than K in a Sub‑Array – Store a sorted multiset in each node (or a bitset) and query by binary searching each node’s list. Complexity stays O(log² n) but demonstrates the tree’s flexibility beyond simple sums.

Why This New Power Matters

Armed with a segment tree, you can turn any “brute‑force over a range” problem into a logarithmic‑time solution, even when the data mutates. Imagine building a real‑time analytics dashboard that shows the total sales of any product category over the last hour, while orders keep pouring in. Or a game engine that needs to query the total health of units in a rectangular region as they move and take damage. The segment tree is the one ring that binds range queries, updates, and diverse aggregations under a single, efficient abstraction.

The best part? Once you grasp the core idea—pre‑aggregate over intervals and reuse those aggregates—you can swap the combine function (sum → min → max → GCD → custom monoid) and instantly solve a whole family of problems. It’s like leveling up from a basic sword to a versatile, enchanted blade that adapts to any foe.

Your Next Quest

Grab a small array, implement the tree from scratch, and throw in a twist: support range add updates (lazy propagation) instead of point assignments. See how the same structure can be stretched with just a few extra lines.

When you get it working, drop a comment with your favorite variation or a bug you squashed—let’s celebrate the win together!

Happy coding, and may your queries always be logarithmic! 🚀

Top comments (0)