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 “range query” problem in an interview. The interviewer slid a whiteboard marker across the table and said, “You have an array of size n. You need to answer m queries that ask for the sum of elements between indices l and r, and you also have to support point updates.” My brain instantly went to the naïve solution: loop from l to r for every query. That’s O(n) per query, which is fine if m is tiny, but the moment m gets into the hundreds of thousands—or worse, when n itself is huge—the approach collapses like a house of cards in a hurricane. I felt stuck, like I was trying to dig a tunnel with a spoon.

That frustration sparked a question: Is there a way to preprocess the data so that both queries and updates become sub‑linear? I knew prefix sums gave O(1) queries but killed updates (O(n) to rebuild the prefix array). I needed something that could give me the best of both worlds.

The Revelation (The Insight)

The breakthrough came when I realized that the problem isn’t about scanning a flat array—it’s about dividing the array into logical chunks and storing a summary for each chunk. If I could quickly combine the summaries of only the chunks that fully lie inside [l, r] and ignore the rest, I’d avoid looking at every single element.

That’s exactly what a segment tree does. Imagine a binary tree where each node represents a contiguous segment of the original array. The root covers the whole range [0, n‑1]. Its two children split that range in half, and this splitting continues until the leaves each represent a single element.

Why does this work?

  • Every node stores the aggregate (sum, min, max, etc.) of its segment.
  • Any query range [l, r] can be expressed as a union of O(log n) disjoint node segments. This is a property of binary partitioning: as you walk down the tree, you either take‑tree, you only go left or right when the current node’s segment is partially overlapped; otherwise you take the whole node. Because each level of the tree halves the segment size, you can only descend log n levels before you’ve covered the range.
  • Updating a single element means updating the leaf and then recomputing the aggregates on the path back to the root—again only O(log n) nodes.

So the segment tree trades a linear‑size auxiliary structure (about 4 n nodes) for logarithmic query and update times. The preprocessing step—building the tree—is a simple post‑order traversal that computes each parent from its two children, which runs in O(n).

Wielding the Power (Code & Examples)

Let’s see the theory in action with a classic interview problem: range sum query with point updates.

The Struggle (Naïve Approach)

def naive_range_sum(arr, queries):
    # queries = [(l, r, type), ...] where type='sum' or ('update', idx, val)
    res = []
    for q in queries:
        if q[0] == 'sum':
            _, l, r = q
            s = sum(arr[l:r+1])          # O(n) per query
            res.append(s)
        else:  # update
            _, idx, val = q
            arr[idx] = val               # O(1)
    return res
Enter fullscreen mode Exit fullscreen mode

If you have 10⁵ queries on an array of size 10⁵, you’re looking at up to 10¹⁰ operations—definitely not going to pass.

The Victory (Segment Tree)

class SegTree:
    def __init__(self, data):
        self.n = len(data)
        # size 4*n is a safe upper bound for a full binary tree
        self.tree = [0] * (4 * self.n)
        self._build(data, 0, 0, self.n - 1)

    def _build(self, data, node, l, r):
        if l == r:                     # leaf
            self.tree[node] = data[l]
        else:
            mid = (l + r) // 2
            left = node * 2 + 1
            right = node * 2 + 2
            self._build(data, left, l, mid)
            self._build(data, right, mid + 1, r)
            self.tree[node] = self.tree[left] + self.tree[right]

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

    def _update(self, node, l, r, idx, val):
        if l == r:                     # leaf to update
            self.tree[node] = val
            return
        mid = (l + r) // 2
        left = node * 2 + 1
        right = node * 2 + 2
        if idx <= mid:
            self._update(left, l, mid, idx, val)
        else:
            self._update(right, mid + 1, r, idx, val)
        self.tree[node] = self.tree[left] + self.tree[right]

    def query(self, ql, qr):
        return self._query(0, 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 = node * 2 + 1
        right = node * 2 + 2
        return self._query(left, l, mid, ql, qr) + \
               self._query(right, mid + 1, r, ql, qr)
Enter fullscreen mode Exit fullscreen mode

What to watch out for (the traps):

  • Off‑by‑one in the query boundaries. The tree works with inclusive ranges; mixing exclusive ends will give you wrong answers.
  • Forgetting to propagate updates: if you only change the leaf and skip recomputing parents, higher nodes keep stale sums.
  • Using too small an array for the tree. Allocating exactly 2  next_power_of_two(n) − 1 works, but the simple 4  n guard is beginner‑friendly and safe.

A Second Interview Favorite: Range Minimum Query

The same structure works if you store min instead of sum. Just replace the aggregation line in _build, _update, and _query with min(left, right). No other changes needed—proof that the segment tree is a generic divide‑and‑conquer tool.

Why This New Power Matters

With a segment tree in your toolbox, you turn problems that once felt like grinding through a mountain of data into swift, elegant traversals. Imagine building a live leaderboard where scores update constantly and you need to fetch the top‑k sum in a range—now each operation is O(log n). Or think of a scheduling system that must quickly find the earliest free slot in a time interval; a segment tree storing the minimum end‑time does exactly that in logarithmic time.

The beauty isn’t just the speed‑up; it’s the mindset shift. You start seeing any range‑based aggregate as a hierarchy of summaries, and you instinctively reach for a tree when the flat array feels insufficient. That shift opens doors to more advanced structures—fenwick trees, interval trees, even segment trees with lazy propagation for range updates.

So next time you see a problem that asks, “Give me the sum/min/max between L and R after many updates,” don’t reach for the brute‑force loop. Reach for the segment tree, feel the power of logarithmic splits, and watch your solution go from “just works” to “blazingly fast.”

Your Turn

Try implementing a segment tree that supports range addition updates (add a value to every element in [l, r]) and range sum queries using lazy propagation. Drop your solution in the comments or share a link—I’d love to see how you wield this new spell!

Happy coding! 🚀

Top comments (0)