DEV Community

Timevolt
Timevolt

Posted on

Segment Trees: The Jedi Way to Crush Range Queries

The Quest Begins (The "Why")

I still remember the first time I faced a range‑sum question in an interview. The interviewer slid a whiteboard marker across the table and said, “Given an array, return the sum of elements between indices L and R, and you’ll have to do it a bunch of times.” My gut reaction? Loop from L to R, add them up, and call it a day. Easy, right?

Then came the twist: “You’ll need to answer up to 10⁵ queries on an array of size 10⁵.” Suddenly my O(n) per‑query solution felt like trying to bail out a sinking ship with a teaspoon. I could hear the clock ticking, and my confidence started to wobble. I spent a weekend grinding through brute‑force attempts, watching my test cases time out one after another. It was frustrating, but it also lit a fire: there had to be a smarter way to preprocess the data so each query could be answered in a blink.

That’s when I stumbled upon segment trees. At first glance they looked like a tangled binary mess, but once I peeled back the layers, the idea clicked like a lock finally turning. Let me walk you through that “aha!” moment.

The Revelation (The Insight)

So why does a segment tree work? Think of the array not as a flat line but as a hierarchy of intervals. At the very top we have the whole range [0, n‑1]. Below that we split it into two halves, then each half into quarters, and so on, until we reach individual elements. This creates a binary tree where every node stores the summary of its interval—be it sum, min, max, or any associative operation you like.

The magic lies in two facts:

  1. Associativity lets us combine. If we know the summary of [L, M] and [M+1, R], we can get the summary of [L, R] by just applying the operation once (e.g., sum_left + sum_right). No need to look at the individual elements again.
  2. Any query range can be covered by O(log n) nodes. When we ask for [L, R], we walk down the tree, picking nodes that are fully inside the query and skipping those that are completely outside. Because each step halves the remaining search space, we visit at most two nodes per tree level—hence O(log n).

In short, the tree does the heavy lifting once during construction, and then each query merely stitches together a handful of pre‑computed answers. It’s like having a cheat sheet that tells you the sum of any block instantly, and you only need to glue a few of those blocks together to answer arbitrary ranges.

I was shocked when I realized the build step itself is linear—just a post‑order traversal that fills each node from its children. No nested loops, no magic, just pure divide‑and‑conquer.

Wielding the Power (Code & Examples)

The Struggle: Naïve Approach

def range_sum_naive(arr, L, R):
    return sum(arr[L:R+1])          # O(R-L+1) time
Enter fullscreen mode Exit fullscreen mode

If you call this 10⁵ times on a 10⁵‑length array, you’re looking at up to 10¹⁰ operations—definitely not interview‑friendly.

The Victory: Segment Tree for Range Sum

Below is a clean, iterative‑friendly implementation. I’ve kept the comments tight so you can see the logic at a glance.

class SegTree:
    def __init__(self, data, func=sum):
        """Build a segment tree for `data` using associative `func`."""
        self.n = len(data)
        self.func = func
        # size of the tree array (next power of two * 2)
        self.size = 1
        while self.size < self.n:
            self.size <<= 1
        self.tree = [0] * (2 * self.size)
        # fill 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.func(self.tree[2 * i], self.tree[2 * i + 1])

    def query(self, l, r):
        """Return func(arr[l..r]) inclusive."""
        l += self.size
        r += self.size + 1      # make r exclusive
        res_left = self.func.__defaults__[0] if self.func.__defaults__ else 0
        res_right = res_left
        while l < r:
            if l & 1:
                res_left = self.func(res_left, self.tree[l])
                l += 1
            if r & 1:
                r -= 1
                res_right = self.func(self.tree[r], res_right)
            l >>= 1
            r >>= 1
        return self.func(res_left, res_right)

    def update(self, idx, value):
        """Set arr[idx] = value and refresh the tree."""
        pos = self.size + idx
        self.tree[pos] = value
        pos >>= 1
        while pos:
            self.tree[pos] = self.func(self.tree[2 * pos], self.tree[2 * pos + 1])
            pos >>= 1
Enter fullscreen mode Exit fullscreen mode

Why this works:

  • The tree is stored in a flat array where children of node i are 2*i and 2*i+1.
  • Building walks from the leaves upward, guaranteeing each parent holds the func of its two children—exactly the invariant we need.
  • The query loop climbs up only when the current segment is wholly inside or outside the target range, collecting partial results on the way. Because each iteration moves l and r toward each other by at least one level, we hit at most 2 * log₂(size) nodes.
  • Update follows the same path from leaf to root, fixing only O(log n) nodes.

Common Traps

Trap What happens How to avoid
Off‑by‑one on the query bounds You accidentally include/exclude an element, leading to wrong sums. Remember that the internal query expects [l, r) after the leaf offset; adjust the caller accordingly (r+1 for inclusive).
Using a non‑associative function The tree’s combine step gives nonsense because (a op b) op c ≠ a op (b op c). Stick to sum, min, max, gcd, bitwise OR/AND/XOR, etc. If you need something else, consider a Fenwick tree or a different structure.
Forgetting to rebuild after an update Stale values linger, causing later queries to return outdated results. Always walk up from the modified leaf to the root, recomputing each parent.

Two Interview‑Style Problems

  1. Range Sum Query (Classic)

    Prompt: Given an integer array, support update(i, val) and sumRange(l, r) efficiently.

    Solution: Use the SegTree above with func=lambda a,b: a+b. Build O(n), each query/update O(log n).

  2. Range Minimum Query

    Prompt: Same array, but you need to return the minimum in a range after point updates.

    Solution: Instantiate SegTree(arr, func=min). The same code works; only the combine function changes.

Both problems appear regularly on platforms like LeetCode (307. Range Sum Query - Mutable, 239. Sliding Window Maximum can be adapted with a segment tree for max).

Why This New Power Matters

Mastering the segment tree isn’t just about clearing one interview question—it’s about adding a versatile tool to your problem‑solving belt. Once you internalize the “divide‑and‑conquer + summary” pattern, you’ll start seeing it everywhere:

  • Dynamic order statistics (count of numbers < k in a range) → segment tree of sorted vectors or a BIT of frequencies.
  • Geometric queries (max y‑coordinate of points in a vertical strip) → segment tree storing max per x‑interval.
  • String problems (longest palindrome in a substring) → segment tree storing hash prefixes for O(log n) checks.

In other words, the segment tree teaches you to preprocess information in a way that makes arbitrary interval queries cheap. That mindset shifts you from “let’s loop every time” to “let’s build a structure once and reuse it.”

And the best part? The code is short enough to type out during a whiteboard interview, yet powerful enough to handle constraints that would make a naive solution melt down.

Your Turn

Here’s a fun challenge: take the segment tree above and adapt it to answer range XOR queries (arr[l] ^ arr[l+1] ^ … ^ arr[r]). Update the func to bitwise XOR, test it on random arrays, and verify that both updates and queries stay O(log n).

If you build it, drop a link to your gist in the comments—I’d love to see your implementation and hear any tweaks you discovered. Happy coding, and may your queries always be logarithmic!

Top comments (0)