Mustaque Nadim Academy
Range Queries

Segment Trees

You need the sum (or min, or max) of any range in a constantly-changing array — a segment tree answers and updates in log time.

The problem

You are building the leaderboard for a game. Scores sit in an array, one slot per player, and the dashboard keeps asking two questions all day long: "what's the total score of players 40 through 90?" and "player 57 just scored, bump their value." Both happen thousands of times a second.

If you had to pick one thing to make fast, you couldn't — the reads and the writes are tangled together. Make lookups instant and every write pays for it. Make writes instant and every lookup crawls. You need both cheap at once.

A first attempt

The obvious approach: keep the plain array. A range sum is just a loop.

def range_sum(a, l, r):
    return sum(a[l:r + 1])   # O(n)

def update(a, i, value):
    a[i] = value             # O(1)

Updates are perfect — O(1). But every range sum walks up to n elements, so a query is O(n). With thousands of queries over a large array, the dashboard stalls.

"Fine," you say, "I'll precompute prefix sums." Now range_sum is O(1)... but a single update forces you to rebuild every prefix after it — O(n) per write. You just moved the pain. One structure is fast to read, the other fast to write. You want one that is good at both.

The insight

Stop treating the array as flat. Cache the answer for chunks of it, arranged as a binary tree.

The root stores the sum of the whole array. Its two children store the sums of the left half and the right half. Their children split again, and so on until each leaf holds a single element. Any range you ask about can be assembled from a handful of these pre-summed chunks — never more than about 2 log n of them. And when one element changes, only the chunks that contain it need fixing: exactly the path from that leaf to the root, again log n nodes.

That is a segment tree: reads and writes both become O(log n).

How it works

Lay the tree over the array

Node 1 covers the whole range [0, n-1]. A node covering [lo, hi] splits at mid = (lo + hi) / 2: its left child (index 2*node) covers [lo, mid], its right child (2*node+1) covers [mid+1, hi]. Leaves cover a single index.

             [0..7]=36
          /            \
    [0..3]=10        [4..7]=26
     /     \          /      \
 [0..1]   [2..3]   [4..5]   [6..7]
   3        7        11       15
  / \      / \      / \      / \
 1   2    3   4    5   6    7   8

Build from the bottom up

Recurse to the leaves, write each element, then on the way back up set every internal node to the sum of its two children. One pass, O(n).

Point update walks one path

To change index i, recurse toward the leaf that owns it, overwrite the leaf, and re-sum each ancestor as you unwind. Only the nodes on that root-to-leaf path change — O(log n).

Range query stitches chunks together

To sum [l, r], visit from the root. If a node's range is fully inside [l, r], return its stored sum whole. If it's fully outside, return the identity (0). Otherwise split and combine both halves. The query touches O(log n) nodes.

The code

class SegmentTree:
    def __init__(self, data):
        self.n = len(data)
        self.tree = [0] * (4 * self.n)
        if self.n:
            self._build(data, 1, 0, self.n - 1)

    def _build(self, data, node, lo, hi):
        if lo == hi:
            self.tree[node] = data[lo]
            return
        mid = (lo + hi) // 2
        self._build(data, 2 * node, lo, mid)
        self._build(data, 2 * node + 1, mid + 1, hi)
        self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

    def update(self, i, value):
        self._update(1, 0, self.n - 1, i, value)

    def _update(self, node, lo, hi, i, value):
        if lo == hi:
            self.tree[node] = value
            return
        mid = (lo + hi) // 2
        if i <= mid:
            self._update(2 * node, lo, mid, i, value)
        else:
            self._update(2 * node + 1, mid + 1, hi, i, value)
        self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

    def query(self, l, r):  # inclusive sum over [l, r]
        return self._query(1, 0, self.n - 1, l, r)

    def _query(self, node, lo, hi, l, r):
        if r < lo or hi < l:
            return 0
        if l <= lo and hi <= r:
            return self.tree[node]
        mid = (lo + hi) // 2
        return (self._query(2 * node, lo, mid, l, r)
                + self._query(2 * node + 1, mid + 1, hi, l, r))
class SegmentTree {
  private n: number;
  private tree: number[];

  constructor(data: number[]) {
    this.n = data.length;
    this.tree = new Array(4 * this.n).fill(0);
    if (this.n > 0) this.build(data, 1, 0, this.n - 1);
  }

  private build(data: number[], node: number, lo: number, hi: number): void {
    if (lo === hi) {
      this.tree[node] = data[lo];
      return;
    }
    const mid = (lo + hi) >> 1;
    this.build(data, 2 * node, lo, mid);
    this.build(data, 2 * node + 1, mid + 1, hi);
    this.tree[node] = this.tree[2 * node] + this.tree[2 * node + 1];
  }

  update(i: number, value: number): void {
    this.updateNode(1, 0, this.n - 1, i, value);
  }

  private updateNode(node: number, lo: number, hi: number, i: number, value: number): void {
    if (lo === hi) {
      this.tree[node] = value;
      return;
    }
    const mid = (lo + hi) >> 1;
    if (i <= mid) this.updateNode(2 * node, lo, mid, i, value);
    else this.updateNode(2 * node + 1, mid + 1, hi, i, value);
    this.tree[node] = this.tree[2 * node] + this.tree[2 * node + 1];
  }

  query(l: number, r: number): number {
    return this.queryNode(1, 0, this.n - 1, l, r);
  }

  private queryNode(node: number, lo: number, hi: number, l: number, r: number): number {
    if (r < lo || hi < l) return 0;
    if (l <= lo && hi <= r) return this.tree[node];
    const mid = (lo + hi) >> 1;
    return (
      this.queryNode(2 * node, lo, mid, l, r) +
      this.queryNode(2 * node + 1, mid + 1, hi, l, r)
    );
  }
}
class SegmentTree {
    private final int n;
    private final long[] tree;

    SegmentTree(int[] data) {
        n = data.length;
        tree = new long[4 * Math.max(1, n)];
        if (n > 0) build(data, 1, 0, n - 1);
    }

    private void build(int[] data, int node, int lo, int hi) {
        if (lo == hi) {
            tree[node] = data[lo];
            return;
        }
        int mid = (lo + hi) >>> 1;
        build(data, 2 * node, lo, mid);
        build(data, 2 * node + 1, mid + 1, hi);
        tree[node] = tree[2 * node] + tree[2 * node + 1];
    }

    void update(int i, int value) {
        update(1, 0, n - 1, i, value);
    }

    private void update(int node, int lo, int hi, int i, int value) {
        if (lo == hi) {
            tree[node] = value;
            return;
        }
        int mid = (lo + hi) >>> 1;
        if (i <= mid) update(2 * node, lo, mid, i, value);
        else update(2 * node + 1, mid + 1, hi, i, value);
        tree[node] = tree[2 * node] + tree[2 * node + 1];
    }

    long query(int l, int r) {
        return query(1, 0, n - 1, l, r);
    }

    private long query(int node, int lo, int hi, int l, int r) {
        if (r < lo || hi < l) return 0;
        if (l <= lo && hi <= r) return tree[node];
        int mid = (lo + hi) >>> 1;
        return query(2 * node, lo, mid, l, r) + query(2 * node + 1, mid + 1, hi, l, r);
    }
}
#include <stdlib.h>

typedef struct {
    int n;
    long long *tree;
} SegTree;

static void build(SegTree *st, const int *data, int node, int lo, int hi) {
    if (lo == hi) {
        st->tree[node] = data[lo];
        return;
    }
    int mid = (lo + hi) / 2;
    build(st, data, 2 * node, lo, mid);
    build(st, data, 2 * node + 1, mid + 1, hi);
    st->tree[node] = st->tree[2 * node] + st->tree[2 * node + 1];
}

SegTree *seg_create(const int *data, int n) {
    SegTree *st = malloc(sizeof(SegTree));
    st->n = n;
    st->tree = calloc(4 * n, sizeof(long long));
    if (n > 0) build(st, data, 1, 0, n - 1);
    return st;
}

static void seg_set(SegTree *st, int node, int lo, int hi, int i, int value) {
    if (lo == hi) {
        st->tree[node] = value;
        return;
    }
    int mid = (lo + hi) / 2;
    if (i <= mid) seg_set(st, 2 * node, lo, mid, i, value);
    else seg_set(st, 2 * node + 1, mid + 1, hi, i, value);
    st->tree[node] = st->tree[2 * node] + st->tree[2 * node + 1];
}

void seg_update(SegTree *st, int i, int value) {
    seg_set(st, 1, 0, st->n - 1, i, value);
}

static long long seg_sum(SegTree *st, int node, int lo, int hi, int l, int r) {
    if (r < lo || hi < l) return 0;
    if (l <= lo && hi <= r) return st->tree[node];
    int mid = (lo + hi) / 2;
    return seg_sum(st, 2 * node, lo, mid, l, r)
         + seg_sum(st, 2 * node + 1, mid + 1, hi, l, r);
}

long long seg_query(SegTree *st, int l, int r) {
    return seg_sum(st, 1, 0, st->n - 1, l, r);
}
#include <vector>
using namespace std;

class SegmentTree {
    int n;
    vector<long long> tree;

    void build(const vector<int> &data, int node, int lo, int hi) {
        if (lo == hi) {
            tree[node] = data[lo];
            return;
        }
        int mid = (lo + hi) / 2;
        build(data, 2 * node, lo, mid);
        build(data, 2 * node + 1, mid + 1, hi);
        tree[node] = tree[2 * node] + tree[2 * node + 1];
    }

    void update(int node, int lo, int hi, int i, int value) {
        if (lo == hi) {
            tree[node] = value;
            return;
        }
        int mid = (lo + hi) / 2;
        if (i <= mid) update(2 * node, lo, mid, i, value);
        else update(2 * node + 1, mid + 1, hi, i, value);
        tree[node] = tree[2 * node] + tree[2 * node + 1];
    }

    long long query(int node, int lo, int hi, int l, int r) {
        if (r < lo || hi < l) return 0;
        if (l <= lo && hi <= r) return tree[node];
        int mid = (lo + hi) / 2;
        return query(2 * node, lo, mid, l, r)
             + query(2 * node + 1, mid + 1, hi, l, r);
    }

public:
    SegmentTree(const vector<int> &data) : n(data.size()), tree(4 * data.size()) {
        if (n > 0) build(data, 1, 0, n - 1);
    }

    void update(int i, int value) { update(1, 0, n - 1, i, value); }

    long long query(int l, int r) { return query(1, 0, n - 1, l, r); }
};

Complexity

OperationTimeSpace
BuildO(n)O(n)
Point updateO(log n)O(1)
Range queryO(log n)O(log n) recursion

The tree uses 4n slots — a safe upper bound that guarantees room for every node even when n is not a power of two.

When to use it

Reach for a segment tree when reads and writes are both frequent

If the array is static, prefix sums (O(1) query, O(n) build) win — a segment tree is overkill. The moment you need frequent point updates and frequent range queries on the same data, the segment tree's O(log n) on both sides is what makes it shine. Watch the 4n memory and prefer the iterative bottom-up form if you're memory-bound.

Practice

Recap

  • A segment tree caches range answers in a binary tree so both point updates and range queries run in O(log n).
  • Each node owns a sub-range; internal nodes combine their children with an associative operation (sum, min, max, …).
  • Build is O(n), the tree uses 4n slots, and a query stitches together O(log n) pre-computed chunks.

How is this guide?

Last updated on

On this page