DEV Community

Timevolt
Timevolt

Posted on

Segment Trees: The Matrix of Range Queries

The Quest Begins (The "Why")

Honestly, I still remember the first time I saw a problem that asked for the sum of a sub‑array after a bunch of point updates. My naive solution looped over the range each time – O(n) per query – and it felt like trying to fill a swimming pool with a teaspoon. The test cases kept timing out, and I started to doubt whether I’d ever crack the interview round.

That frustration was the dragon I needed to slay. I needed a data structure that could both update a single element and answer a range aggregate in sub‑linear time, preferably O(log n). The moment I stumbled upon segment trees, it was like Neo seeing the code behind the Matrix – everything snapped into focus.

The Revelation (The Insight)

So why does a segment tree work? Think of the array as a line of warriors. Instead of asking each warrior individually for their strength every time we need a total, we pre‑compute the strength of groups: pairs, then groups of four, then eight, and so on, up to the whole line.

Each node in the tree stores the aggregate (sum, max, min, etc.) of a contiguous segment. The root covers the whole array; its two children split that segment in half; their children split again, and we keep halving until we reach leaves that represent single elements.

Because we always split in half, the height of the tree is ⌈log₂ n⌉. To answer a range query, we walk down from the root, picking only those nodes whose segments lie completely inside the query range. At most two nodes per level are needed – one from the left side, one from the right – giving us O(log n) nodes visited.

Updating a point is just as tidy: we change the leaf and then recompute the aggregates on the path back to the root, again touching only O(log n) nodes.

The beauty is that the tree encodes the divide‑and‑conquer strategy we already know from merge sort or binary search, but it stores the results so we never have to recompute them from scratch.

Wielding the Power (Code & Examples)

Let’s look at a classic interview problem: Range Sum Query – Mutable (LeetCode 307). We need to support:

  • update(i, val) – set nums[i] = val
  • sumRange(l, r) – return Σ nums[k] for l ≤ k ≤ r

Below is a clean, 0‑indexed implementation in Python. I’ll point out the traps that caught me the first time.

class SegTree:
    def __init__(self, data):
        """Build the tree in O(n)."""
        self.n = len(data)
        # size = next power of two >= n, tree stored in an array of length 2*size
        self.size = 1
        while self.size < self.n:
            self.size <<= 1
        self.tree = [0] * (2 * self.size)
        # load leaves
        self.tree[self.size:self.tree[size + self.data
        # build internal nodes
        for i in range(self.size -  : self.size + self.n] = data
        # build parents
        for i in range(self.size - 1, 0, -1):
            self.tree[i] = self.tree[i << 1] + self.tree[i << 1 | 1]

    def update(self, idx, value):
        """Point update: O(log n)."""
        pos = idx + self.size
        self.tree[pos] = value
        # walk up, recomputing
        while pos > 1:
            pos >>= 1
            self.tree[pos] = self.tree[pos << 1] + self.tree[pos << 1 | 1]

    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 is not just copy‑paste

  • Size calculation – I used the smallest power of two ≥ n. If you simply allocate 4*n you’ll waste memory but still work; however, picking a power of two makes the child‑index math (i<<1 and i<<1|1) trivial and avoids off‑by‑one bugs.
  • Leaf loading – Notice the slice assignment self.tree[self.size : self.size + self.n] = data. Forgetting to offset by self.size writes into the internal nodes and corrupts the tree.
  • Query loop – The trick of turning the right bound into an exclusive index (r += self.size + 1) lets us use the classic “while l < r” pattern. If you keep r inclusive you’ll either miss the last element or double‑count.
  • Update propagation – After changing a leaf we must walk all the way to the root (while pos > 1). Skipping this step leaves stale aggregates higher up, a mistake that haunted me for an hour of debugging.

A second flavor: Range Maximum Query

The same skeleton works if we replace the associative operation with max. Just change the build and update lines:

self.tree[i] = max(self.tree[i << 1], self.tree[i << 1 | 1])
...
self.tree[pos] = max(self.tree[pos << 1], self.tree[pos << 1 | 1])
...
res = max(res, self.tree[l])
Enter fullscreen mode Exit fullscreen mode

Now query(l, r) returns the maximum in O(log n). This is exactly what interviewers love to ask when they want to see if you grasp the generic nature of segment trees.

Why This New Power Matters

Armed with a segment tree, you can turn any problem that repeatedly asks for an associative range aggregate (sum, min, max, GCD, bitwise OR, etc.) into a solution that scales. No more O(n²) nightmares; you’ll handle 10⁵ updates and queries comfortably within the time limit.

It also opens the door to lazy propagation for range updates – but that’s a quest for another day. For now, you have a reliable, reusable spell in your toolkit: build in O(n), update and query in O(log n).

Imagine walking into an interview, writing the segment tree class on the whiteboard, and watching the interviewer’s eyes light up as you explain why each step is logarithmic. That’s the kind of moment that makes the grind worthwhile.

Your Turn

Try implementing a segment tree that supports range sum query and point update in a language you’re less comfortable with (maybe Go or Rust). Then, twist it: make it return the number of distinct values in a range by storing a bitset in each node (hint: use bitwise OR as the aggregate).

Drop your solution in the comments or tweet it with #SegmentTreeQuest – I’d love to see how you wield the power! Happy coding!

Top comments (0)