DEV Community

Timevolt
Timevolt

Posted on

Segment Trees: The One Ring to Rule Range Queries

The Quest Begins (The “Why”)

I still remember the first time I faced a problem that asked for the sum of numbers in a sub‑array, and then to update a single element and ask again. It was on a whiteboard during a mock interview, and my gut reaction was to loop from l to r each time—O(n) per query. The interviewer nodded, then threw in a twist: “What if there are 10⁵ queries?” My heart sank. I felt like Frodo staring at Mordor, knowing the journey would be long and painful if I kept taking the scenic route.

That moment sparked a quest: Is there a way to answer range sum (or min, max, gcd, etc.) queries faster than linear time, while still supporting point updates? The answer, as many of you might already suspect, lives in a data structure that feels almost magical: the Segment Tree.

The Revelation (The Insight)

Here’s the insight that turned the tide for me: a segment tree is simply a binary tree that stores aggregated information for intervals of the original array. Each node represents a segment, and the value stored in that node is the result of applying our chosen operation (sum, min, max…) to all elements in that segment.

Why does this help? Because any arbitrary range [l, r] can be broken down into O(log n) disjoint segments that are exactly represented by nodes in the tree. Think of it like packing a suitcase: instead of shoving every shirt individually (the naïve O(n) way), you fold them into a few neat bundles that together cover the whole wardrobe. Those bundles are the tree nodes we visit during a query.

The building process is also elegant: we start at the leaves (each leaf holds a single array element) and work our way up, computing each parent as the operation of its two children. This bottom‑up construction touches each element a constant number of times → O(n) time and O(n) space.

When we need to update a single position, we only have to walk from the leaf up to the root, updating the stored aggregates along the path—again O(log n). No need to rebuild the whole tree.

That’s the “why”: the segment tree trades a little extra space for logarithmic query/update time by pre‑aggregating information in a hierarchy that mirrors the way we can decompose any interval.

Wielding the Power (Code & Examples)

Let’s see the theory in code. I’ll use Python for readability, but the same idea translates to C++, Java, or JavaScript.

The Struggle: Naïve Solution

def range_sum_naive(arr, l, r):
    return sum(arr[l:r+1])          # O(r-l+1) → O(n) in worst case

def update_naive(arr, idx, val):
    arr[idx] = val                  # O(1) but queries stay slow
Enter fullscreen mode Exit fullscreen mode

If you have q queries, the total becomes O(n·q)—unsuitable for large inputs.

The Victory: Segment Tree

class SegTree:
    def __init__(self, data, func=lambda a, b: a + b):
        """func is the associative operation (sum, min, max, etc.)"""
        self.n = len(data)
        self.func = func
        # size 2*n is enough for a full binary tree stored in an array
        self.tree = [0] * (2 * self.n)
        # build leaves
        self.tree[self.n:self.n + self.n] = data
        # build internal nodes
        for i in range(self.n - 1, 0, -1):
            self.tree[i] = self.func(self.tree[2 * i], self.tree[2 * i + 1])

    def update(self, idx, value):
        """Point update: set arr[idx] = value"""
        pos = self.n + idx
        self.tree[pos] = value
        # walk up
        while pos > 1:
            pos //= 2
            self.tree[pos] = self.func(self.tree[2 * pos], self.tree[2 * pos + 1])

    def query(self, l, r):
        """Range query on [l, r)  (half‑open interval for simplicity)"""
        l += self.n
        r += self.n
        res_left = None   # identity will depend on func; we handle generically
        res_right = None
        while l < r:
            if l & 1:
                res_left = self.tree[l] if res_left is None else self.func(res_left, self.tree[l])
                l += 1
            if r & 1:
                r -= 1
                res_right = self.tree[r] if res_right is None else self.func(self.tree[r], res_right)
            l //= 2
            r //= 2
        # combine left and right accumulators
        if res_left is None:
            return res_right
        if res_right is None:
            return res_left
        return self.func(res_left, res_right)
Enter fullscreen mode Exit fullscreen mode

Why This Works (Quick Walk‑through)

  • Build: Leaves (tree[n … 2n-1]) hold the raw values. Each internal node at index i stores func(tree[2i], tree[2i+1]). By the time we finish the loop, every node knows the aggregate of its segment.
  • Update: Changing a leaf means all ancestors that included that leaf might change. We climb the tree, recomputing each parent from its two children—log n steps.
  • Query: The classic two‑pointer method walks up the tree, picking whole segments that lie completely inside [l, r). Because we always move to the parent after processing a child, we visit at most two nodes per level → O(log n).

Common Traps (The “Bosses” to Avoid)

  1. Off‑by‑one on the interval – Remember the query method above expects a half‑open range [l, r). If you accidentally pass an inclusive right bound, you’ll either miss an element or read out of bounds. I once spent an hour debugging why my sum was always one short; the fix was simply r += 1 before calling query.
  2. Assuming the operation is invertible – The segment tree only needs the operation to be associative (and have an identity if you want a clean query). It does not need to be invertible, unlike a Fenwick tree for sum. Trying to “subtract” contributions when the operation is min or max leads to nonsense. Stick to the generic func pattern.

Interview‑Style Problems

Problem 1 – Range Sum Query with Point Updates

Given an array a of size n (≤10⁵), process q queries (≤10⁵) of two types: 1 i v – set a[i] = v; 2 l r – output Σₖ₌ₗᵣ a[k].

Solution: Build a SegTree with func = lambda x, y: x + y. Each query runs in O(log n). Total O((n+q) log n) → easily fits limits.

Problem 2 – Range Minimum Query

Same constraints, but query type 2 asks for the minimum in [l, r].

Solution: Reuse the exact same SegTree, just change func = lambda x, y: min(x, y). The update logic stays identical because we only recompute parents via min. This shows the power of the abstraction: one data structure, many problems.

Both problems are classic on platforms like LeetCode (“Range Sum Query – Mutable”, “Range Minimum Query – Mutable”) and often appear in tech interviews to test whether you know a logarithmic‑time structure beyond a simple prefix sum.

Why This New Power Matters

Mastering the segment tree feels like unlocking a new spell in your developer’s grimoire. Suddenly, problems that once required brute‑force loops or painful prefix‑sum hacks become clean, efficient, and elegant. You can handle dynamic frequencies, support range gcd/lcm queries, or even build a lazy‑propagation segment tree for range updates—all built on the same foundation.

The best part? The concept scales. Once you grasp the idea of storing aggregates in a tree, you can adapt it to segment trees with lazy propagation, persistent segment trees, or even higher‑dimensional variants (like a 2‑D seg tree for sub‑matrix sums). It’s a stepping stone to many advanced techniques.

Your Turn

Take a minute and try implementing a SegTree for range maximum query with point updates on your own favorite language. Then, twist it: make the query return the second maximum in a range (hint: store both the top two values in each node). Share your solution or a snippet in the comments—I’d love to see how you’ve adapted the One Ring to your own quest!

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

Top comments (0)