Skip to main content

Range Query Trees

Every structure on this page exists to answer one shape of question: give me a fold over a range, and let me change an element, over and over, on an array you are not allowed to re-scan each time. Arrays are perfect at one of those two jobs and terrible at the other. This page is about the structure that is merely good at both, which turns out to be exactly what you want the moment both operations are live in the same problem.

1. The problem: queries and updates at the same time

Two operations, repeated, on the same array:

  • update(i, val) - change one element.
  • query(l, r) - fold a range: sum it, find its minimum, and so on.

If you only ever do one of these, the answer is trivial and you should not be reading the rest of this page:

Structureupdate(i, val)query(l, r)When it is enough
Plain arrayO(1) - write the slotO(n) - rescan the rangeUpdates are frequent, queries are rare
Prefix sum arrayO(n) - rebuild every suffixO(1) - two lookups, subtractedThe array is built once and never changes
Segment treeO(log n)O(log n)Both operations are live - interleaved, and neither is rare
A segment tree is not a faster prefix sum. It is what you build when neither extreme - "updates are free" nor "the array never changes" - is true.

If your problem statement says "you are also given k update operations," that sentence is the whole justification for the tree. Skip it and either the queries or the updates degrade to O(n), and O(n) times O(q) queries is the actual O(n * q) your dense test case is timing out on.

2. Prefix sums, and exactly where they break

A prefix sum array P where P[i] = a[0] + a[1] + ... + a[i-1] turns any range sum into a subtraction: sum(l, r) = P[r+1] - P[l]. One array, built once, O(1) per query forever - as long as "forever" does not include a write.

301142135495034891423APP[2] = 4P[6] = 23sum(2,5) = 23 - 4 = 19

The break is a single write. Change a[2], and every prefix sum from P[3] onward is now wrong - the entire structure has to be rebuilt from that point on, which is O(n). Prefix sums buy O(1) queries by baking the entire history of the array into every entry, and baking is not something you can partially undo.

Prefix sums assume the array is frozen

A prefix-sum solution silently assumes the array never changes again. It is the correct answer to "range sum queries, no updates" (LeetCode 303) and the wrong answer the instant the problem adds "range sum query - mutable" (LeetCode 307), even though the two problem statements look almost identical. Read the constraints for the word "update" before reaching for P.

3. Segment trees

A segment tree fixes this by never fully committing to either extreme. Instead of one array that describes the whole range, it builds a binary tree in which every node owns an interval and stores the fold of that interval - the sum, the min, whatever the query needs.

  • The root owns the whole array, [0, n).
  • Every internal node splits its interval in half and hands the halves to its two children.
  • Every leaf owns a single index.
[0,1][2,3][0,3][4,5][6,7][4,7][0,7]

Building it is a post-order walk: each node's value is the fold of its children, so the leaves have to exist before any internal node's value does.

class SegmentTree:
def __init__(self, arr, merge=lambda a, b: a + b, identity=0):
self.n = len(arr)
self.merge = merge
self.identity = identity
self.tree = [identity] * (4 * self.n) # 4n is a safe upper bound
if self.n:
self._build(arr, 0, 0, self.n - 1)
 
def _build(self, arr, node, lo, hi):
if lo == hi:
self.tree[node] = arr[lo]
return
mid = (lo + hi) // 2
self._build(arr, 2 * node + 1, lo, mid)
self._build(arr, 2 * node + 2, mid + 1, hi)
self.tree[node] = self.merge(self.tree[2 * node + 1], self.tree[2 * node + 2])

A point update walks down to the one leaf that owns index i, changes it, and recomputes every ancestor on the way back up - O(log n) nodes, one per level.

def update(self, i, val):
self._update(0, 0, self.n - 1, i, val)
 
def _update(self, node, lo, hi, i, val):
if lo == hi:
self.tree[node] = val
return
mid = (lo + hi) // 2
if i <= mid:
self._update(2 * node + 1, lo, mid, i, val)
else:
self._update(2 * node + 2, mid + 1, hi, i, val)
self.tree[node] = self.merge(self.tree[2 * node + 1], self.tree[2 * node + 2])

The query is the interesting part. query(l, r) does not walk down to every leaf in [l, r] - it stops as soon as a node's interval fits entirely inside [l, r], and uses that node's precomputed value directly.

def query(self, l, r):
return self._query(0, 0, self.n - 1, l, r)
 
def _query(self, node, lo, hi, l, r):
if r < lo or hi < l:
return self.identity # no overlap at all
if l <= lo and hi <= r:
return self.tree[node] # this node's interval is fully inside [l, r]
mid = (lo + hi) // 2
left = self._query(2 * node + 1, lo, mid, l, r)
right = self._query(2 * node + 2, mid + 1, hi, l, r)
return self.merge(left, right)
[0,1][2,3][0,3][4,5][6,7][4,7][0,7][1,1][6,6]4 canonical nodes cover [1,6], not 6 leaves

This is the whole reason the query is O(log n): at every level of the tree, the range [l, r] slices at most two nodes in half (one at its left edge, one at its right edge) and every node strictly between those cuts is either fully inside or fully outside. A tree of height log n has at most 2 * log n "boundary" nodes total, and everything else resolves in one comparison.

The only requirement on merge is that it be associative.

Nothing above depends on it being addition. Swap merge for min, max, gcd, bitwise |, or even 2x2 matrix multiplication, and every line of build, update and query still works unchanged - because all a segment tree ever does is fold an associative operation over a decomposition of the range, and associativity is exactly the property that makes "fold the pieces in any grouping" give the same answer. This is the generalisation worth keeping: "segment tree" is a shape, not a sum.

mergeidentityAnswers
a + b0range sum
min(a, b)+infrange minimum (RMQ)
max(a, b)-infrange maximum
gcd(a, b)0range GCD
a | b0range OR (bitmask problems)
2x2 matrix productidentity matrixrange Fibonacci-style linear recurrences

4. Lazy propagation

Point updates are O(log n), but a range update - "add 5 to every element from index 2 to 6" - naively touches every leaf in that range, which is O(n). Lazy propagation fixes this the same way the tree fixed range queries: stop at the first node whose interval fits inside the update range, and defer the rest.

Every node gets a lazy tag. Setting a node's tag does not mean "apply this update now" - it means "this whole subtree owes an update it has not been told about yet." The node's own stored value is updated immediately (it represents its whole interval, so the update applies to it in one step), but the children are left untouched until something actually needs to look inside them.

class LazySegmentTree:
def __init__(self, n):
self.n = n
self.tree = [0] * (4 * n)
self.lazy = [0] * (4 * n)
 
def _push_down(self, node, lo, hi):
if self.lazy[node] == 0:
return
mid = (lo + hi) // 2
for child, (clo, chi) in ((2 * node + 1, (lo, mid)), (2 * node + 2, (mid + 1, hi))):
self.tree[child] += self.lazy[node] * (chi - clo + 1)
self.lazy[child] += self.lazy[node] # the debt passes down, unpaid
self.lazy[node] = 0 # this node's debt is now clear
 
def update_range(self, l, r, val, node=0, lo=None, hi=None):
if lo is None:
lo, hi = 0, self.n - 1
if r < lo or hi < l:
return
if l <= lo and hi <= r:
self.tree[node] += val * (hi - lo + 1)
self.lazy[node] += val # defer telling the children
return
self._push_down(node, lo, hi) # about to look inside - pay the debt first
mid = (lo + hi) // 2
self.update_range(l, r, val, 2 * node + 1, lo, mid)
self.update_range(l, r, val, 2 * node + 2, mid + 1, hi)
self.tree[node] = self.tree[2 * node + 1] + self.tree[2 * node + 2]
 
def query(self, l, r, node=0, lo=None, hi=None):
if lo is None:
lo, hi = 0, self.n - 1
if r < lo or hi < l:
return 0
if l <= lo and hi <= r:
return self.tree[node]
self._push_down(node, lo, hi) # same rule: looking inside pays the debt
mid = (lo + hi) // 2
return self.query(l, r, node=2 * node + 1, lo=lo, hi=mid) + \
self.query(l, r, node=2 * node + 2, lo=mid + 1, hi=hi)
[0,1][2,3][0,3][4,5][6,7][4,7][0,7]tag: +3tag: +3dimmed leaves: value is stale until push_down reaches them
Forgetting to push down before descending

Every function that is about to recurse INTO a node's children must call push_down on that node first. The invariant a lazy segment tree relies on is: a node's own stored value is always correct for its whole interval, but its children's values are only correct once the node's tag has been pushed. If update_range or query reads self.tree[2 * node + 1] without pushing down first, it reads a value that is missing an update that was already recorded one level up - and the bug is silent, because it only shows up on ranges that straddle a previously-tagged boundary, which most test cases will not hit.

Mnemonic

A lazy tag is an IOU, not a lie. The node's own answer is already correct - it is not lying about its interval. The tag just means the children have not been told yet, and the only thing that ever collects the debt is a future visit that needs to look inside.

5. Fenwick trees

A segment tree can do more than a Fenwick tree (any associative op, arbitrary merge functions, lazy propagation) but a Fenwick tree (also called a binary indexed tree, or BIT) does one job - prefix folds of an invertible operation like sum - in less memory and with less code, using a single array the same size as the input.

The entire structure is one index trick: i & -i isolates the lowest set bit of i. In two's-complement, -i is ~i + 1, so every bit below the lowest set bit of i flips to 1 in ~i, then the +1 carries through those flipped bits and stops exactly at the lowest set bit - leaving i & -i equal to just that one bit.

i = 0b0110100 (52)
-i = 0b1001100 (two's complement negation)
i & -i = 0b0000100 (4) - only the lowest set bit survives

That single number, i & -i, is the size of the range that index i is responsible for, and it is what both operations walk by.

class FenwickTree:
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1) # 1-indexed - i & -i is undefined at 0
 
def update(self, i, delta):
i += 1 # shift to 1-indexed
while i <= self.n:
self.tree[i] += delta
i += i & -i # climb to the next index this one reports to
 
def prefix_sum(self, i): # sum of a[0..i], 0-indexed inclusive
i += 1
total = 0
while i > 0:
total += self.tree[i]
i -= i & -i # drop down to the previous range this index owns
return total
 
def range_sum(self, l, r):
return self.prefix_sum(r) - (self.prefix_sum(l - 1) if l > 0 else 0)

update walks up by repeatedly adding i & -i - each step jumps to the next larger index whose range includes i. prefix_sum walks down by repeatedly subtracting i & -i - each step drops to the previous index whose range ends exactly where the current one starts. Both walks take O(log n) steps because each step clears at least one more bit of i.

Segment treeFenwick tree
Operations it needsany associative opinvertible op (sum, XOR) for subtraction-based range queries
Range update + range queryyes, with lazy propagationyes, but needs a second Fenwick array (a known but fiddly trick)
MemoryO(n), with a 4n-sized array in practiceO(n), one array the same size as the input
Code sizelarger - explicit tree, recursive build/querysmaller - one array, two short loops
What it generalises tomin/max/gcd RMQ, matrix exponentiation rangesmostly stays a sum/XOR/count structure
Fenwick range-min does not work

You cannot subtract your way to a range minimum, so a plain Fenwick tree cannot answer "min of a range" the way it answers "sum of a range." prefix_sum(r) - prefix_sum(l-1) works because subtraction undoes addition. There is no operation that undoes min, so range_min(l, r) cannot be recovered from two prefix mins the way range_sum can from two prefix sums. Range minimum needs a segment tree (or a sparse table, if updates are absent).

6. Which structure for which query

You needReach forBecause
Range sum, values only read (no updates)Prefix sum arrayO(1) query, and you never pay for updates you don't have
Range sum, with point updatesFenwick treeSmallest code, smallest memory, and sum is invertible
Range min/max/gcd, with point updatesSegment treeNot invertible, so a Fenwick tree cannot subtract its way to the answer
Range updates AND range queriesSegment tree with lazy propagationBoth operations need to skip touching every leaf; only the tree defers work
Range min/max, NO updates at allSparse tableO(1) query after O(n log n) preprocessing - overlapping-interval doubling beats a tree when nothing ever changes
An arbitrary associative fold (matrix product, custom merge)Segment treeThe only one of these that only requires associativity, not invertibility
Mnemonic

"Can I read once and forget the rest?" - use a prefix structure. "Can I undo the operation with subtraction?" - use a Fenwick tree. "Neither?" - use a segment tree. In that order, because each one is progressively more general and progressively more code, and there is no reason to reach for a segment tree when a five-line Fenwick tree already answers the question.

Where to go next

  • Balanced Trees - the other family of tree built to keep an operation at O(log n) under updates, this time for ordering rather than folding.
  • Heaps - a different kind of range answer: not "any range," just "the current minimum or maximum," in exchange for O(1) peek.
  • Traversal and Recursion - the post-order "build from children" shape that a segment tree's build and update both follow.
  • Trees in the Wild - where a Fenwick-tree- style block decomposition turns up outside interview problems.