DEV Community

Timevolt
Timevolt

Posted on

Segment Trees: The Matrix of 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, over and over again, with updates sprinkled in between. It felt like I was stuck in a never‑ending loop of for i in range(l, r+1): total += arr[i] – O(n) per query, and with up to 10⁵ queries the solution timed out every single time. I was staring at the screen, thinking, “There has to be a smarter way to answer these range questions without scanning the whole array each time.”

That moment was my dragon: a seemingly simple problem that kept biting me because I kept reaching for the brute‑force sword. I needed a data structure that could give me the answer in logarithmic time while still supporting point updates. Enter the segment tree – the tool that turned my O(n·q) nightmare into an O((n+q)·log n) victory.

The Revelation (The Insight)

So why does a segment tree work? Imagine you have an array and you want to know the sum of any interval [l, r]. If you could break that interval into a handful of pre‑computed chunks, you’d only need to add those chunk values together instead of touching every element.

A segment tree is exactly that: a binary tree where each node stores the aggregate (sum, min, max, etc.) of a segment of the original array. The root covers the whole array [0, n‑1]. Its two children cover the left half and the right half, and this keeps splitting until the leaves represent single elements.

The magic lies in two facts:

  1. Every node’s value is a function of its children. If you know the sum of the left child and the sum of the right child, the parent’s sum is just their addition. This means we can build the tree bottom‑up in O(n) time.
  2. Any interval can be represented as O(log n) disjoint nodes. When you walk down the tree to answer [l, r], you either take a whole node (if its segment lies completely inside the query) or you recurse further. Because the tree’s height is log₂n, you’ll visit at most 2·log₂n nodes.

Thus, building the tree costs O(n), each query costs O(log n), and each point update also costs O(log n) – you just update the leaf and propagate the change upward.

Think of it like the Fellowship needing to cross a vast landscape: instead of checking every stone, they rely on a few well‑placed waypoints (the segment tree nodes) that instantly tell them whether they’re safe to pass.

Wielding the Power (Code & Examples)

The Struggle – Naïve Approach

def range_sum_naive(arr, l, r):
    return sum(arr[l:r+1])          # O(n) per query

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

With 10⁵ queries this easily blows past the time limit.

The Victory – Segment Tree

Below is a clean, iterative segment tree implementation (the “bottom‑up” style) that works for sum queries. I love this version because it avoids recursion depth issues and is easy to copy‑paste into an interview setting.

class SegTree:
    def __init__(self, data):
        """Build tree in O(n)."""
        self.n = len(data)
        # size is the next power of two >= n
        self.size = 1
        while self.size < self.n:
            self.size <<= 1
        # tree array: leaves start at index `size`
        self.tree = [0] * (2 * self.size)
        # copy data to leaves
        self.tree[self.size:self.size + self.n] = data
        # build internal nodes
        for i in range(self.size - 1, 0, -1):
            self.tree[i] = self.tree[i << 1] + self.tree[i << 1 | 1]

    # ----- helpers -----
    def _apply(self, pos, value):
        """Set leaf `pos` to `value` and update ancestors."""
        idx = pos + self.size
        self.tree[idx] = value
        idx >>= 1
        while idx:
            self.tree[idx] = self.tree[idx << 1] + self.tree[idx << 1 | 1]
            idx >>= 1

    # ----- public API -----
    def update(self, idx, value):
        """Point update: set arr[idx] = value (O(log n))."""
        self._apply(idx, value)

    def query(self, l, r):
        """Range sum on [l, r] inclusive (O(log n))."""
        l += self.size
        r += self.size + 1   # make r exclusive
        res = 0
        while l < r:
            if l & 1:
                res += self.tree[l]
                l += 1
            if r & 1:
                r -= 1
                res += self.tree[r]
            l >>= 1
            r >>= 1
        return res
Enter fullscreen mode Exit fullscreen mode

Why this works:

  • The leaves (size … size+n-1) hold the original array.
  • Each internal node stores the sum of its two children, so the invariant “node = sum of its segment” holds for the whole tree after the build loop.
  • update changes a leaf and then walks up, recomputing each affected ancestor – exactly the nodes whose segment contains the updated index.
  • query climbs the two pointers l and r toward each other, adding whole nodes whenever the current pointer is a right child (meaning that node’s segment lies completely inside the query range). Because we move up one level each iteration, we touch at most O(log n) nodes.

Common Traps

Trap What happens How to avoid
Forgetting to make r exclusive in the query loop You’ll either miss the last element or go out of bounds Use r += size + 1 (or handle inclusivity explicitly)
Using the wrong size (not a power of two) Children indices break, leading to wrong sums Compute size as the smallest power of two ≥ n
Updating the leaf but not propagating upward Stale values in higher nodes, queries become wrong Always walk up to the root after changing a leaf

Real‑World Interview Problems

  1. Range Sum Query with Updates – Classic LeetCode 307 / “Range Sum Query – Mutable”. The segment tree above solves it in O((n+q)·log n).
  2. Maximum Subarray Sum in a Range – Store four values per node (total sum, max prefix, max suffix, max subarray). The same merge logic works; query returns the max subarray sum for any interval.

Both problems appear frequently in FAANG interviews, and having the segment tree template ready gives you a huge confidence boost.

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 scanning the entire array become trivial: you can answer thousands of range queries while still supporting updates, all without breaking a sweat.

Beyond interviews, segment trees are the backbone of many real‑time systems – think of live leaderboards, dynamic statistics dashboards, or any scenario where you need to aggregate over sliding windows with frequent modifications. Once you internalize the build‑query‑update pattern, you’ll start seeing opportunities to apply it everywhere: min/range, gcd, bitwise OR, even custom monoids.

The best part? The concept scales. If you ever need a 2‑D version (for sub‑matrix sums), you just nest the same idea. Or you can swap the sum operation for any associative function and the same code works.

Your Turn

Grab a piece of paper (or your favorite IDE) and try to implement a segment tree for range minimum query with point updates. Test it on a random array, throw in a bunch of updates and queries, and compare the runtime against the naïve O(n) approach.

When you see the log‑time speed‑up kick in, you’ll know you’ve leveled up. Happy coding, and may your queries always be swift!


P.S. If you enjoyed this, drop a comment with your favorite segment‑tree variant or a tricky interview problem you’ve cracked with it. Let’s keep the quest going!

Top comments (0)