Mustaque Nadim Academy
Range Queries

Segment Tree Queries

Range minimum, range XOR, range LCM — one structure answers them all by combining a handful of nodes.

The problem

Your monitoring service stores one latency reading per minute in a big array. The alerting rules don't want sums — they want the worst case. "What was the maximum latency between 2:00 and 3:15?" "What's the minimum free memory across this window?" Tomorrow product asks for the XOR of a range of flags, and next week someone wants the GCD of a slice.

You already built a segment tree for range sums. It would be maddening to write a brand new tree for every one of these. Surely the same shape can answer all of them?

A first attempt

You could keep a separate precomputed table per question — a sparse table for minimums, a prefix XOR for XOR, and so on. Each is fast to query, but each is its own structure to build and, worse, most of them break the moment a value changes. A sparse table can't absorb an update at all; you'd rebuild it in O(n log n).

Maintaining five bespoke structures side by side is a bug farm. You want one tree whose only per-question difference is a tiny rule for combining two answers.

The insight

Look back at how the sum tree worked. Every node stored left + right. The traversal never cared that the operation was addition — it only needed to combine a left answer with a right answer, and a neutral value to return when a node falls outside the query.

That's the whole requirement. If your operation is associative — combining in any grouping gives the same result — it drops straight into a segment tree. Sum, min, max, XOR, GCD, LCM, matrix product: all associative. Pick the operation and its identity (the value that changes nothing), and the exact same traversal answers your question.

Querymerge(a, b)identity
Suma + b0
Minmin(a, b)+∞
Maxmax(a, b)−∞
XORa ^ b0
GCDgcd(a, b)0

How it works

A range splits into O(log n) canonical nodes

Any query range [l, r] decomposes into at most about 2 log n tree nodes whose ranges are fully inside it and don't overlap. The query's job is to find those nodes and fold them together with merge.

query [1..6] over [0..7]:

          [0..7]
         /      \
    [0..3]      [4..7]
    /   \        /   \
 [0..1] [2..3] [4..5] [6..7]
   /\
  0  1

picked whole:  [2..3]  [4..5]   (fully inside)
descend into:  [0..1] -> node 1 ;  [6..7] -> node 6
result = merge(a[1], a[2..3], a[4..5], a[6])

Three cases at every node

Disjoint (r < lo or hi < l): return the identity. Fully covered (l <= lo and hi <= r): return the node's stored value. Partial overlap: recurse into both children and merge their results.

The identity keeps disjoint branches harmless

Returning +∞ from a min query's out-of-range branch means it never wins a min. The identity is exactly the value that merge(x, identity) == x, so folding in an untouched branch can't corrupt the answer.

Point updates still cost O(log n)

Change a leaf, then re-merge each ancestor on the way up — identical to the sum tree. The merge rule you chose is the only thing that differs.

The code

A range-minimum tree. To get max, XOR, or GCD, change only merge and IDENTITY.

import math

class MinSegmentTree:
    IDENTITY = math.inf

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

    def _merge(self, a, b):
        return min(a, b)

    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._merge(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._merge(self.tree[2 * node], self.tree[2 * node + 1])

    def query(self, 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 self.IDENTITY
        if l <= lo and hi <= r:
            return self.tree[node]
        mid = (lo + hi) // 2
        left = self._query(2 * node, lo, mid, l, r)
        right = self._query(2 * node + 1, mid + 1, hi, l, r)
        return self._merge(left, right)
class MinSegmentTree {
  private static IDENTITY = Infinity;
  private n: number;
  private tree: number[];

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

  private merge(a: number, b: number): number {
    return Math.min(a, b);
  }

  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.merge(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.merge(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 MinSegmentTree.IDENTITY;
    if (l <= lo && hi <= r) return this.tree[node];
    const mid = (lo + hi) >> 1;
    const left = this.queryNode(2 * node, lo, mid, l, r);
    const right = this.queryNode(2 * node + 1, mid + 1, hi, l, r);
    return this.merge(left, right);
  }
}
class MinSegmentTree {
    private static final int IDENTITY = Integer.MAX_VALUE;
    private final int n;
    private final int[] tree;

    MinSegmentTree(int[] data) {
        n = data.length;
        tree = new int[4 * Math.max(1, n)];
        java.util.Arrays.fill(tree, IDENTITY);
        if (n > 0) build(data, 1, 0, n - 1);
    }

    private int merge(int a, int b) {
        return Math.min(a, b);
    }

    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] = merge(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] = merge(tree[2 * node], tree[2 * node + 1]);
    }

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

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

typedef struct {
    int n;
    int *tree;
} MinSeg;

static int merge(int a, int b) {
    return a < b ? a : b;
}

static void build(MinSeg *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] = merge(st->tree[2 * node], st->tree[2 * node + 1]);
}

MinSeg *seg_create(const int *data, int n) {
    MinSeg *st = malloc(sizeof(MinSeg));
    st->n = n;
    st->tree = malloc(sizeof(int) * 4 * n);
    for (int i = 0; i < 4 * n; i++) st->tree[i] = INT_MAX;
    if (n > 0) build(st, data, 1, 0, n - 1);
    return st;
}

static void seg_set(MinSeg *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] = merge(st->tree[2 * node], st->tree[2 * node + 1]);
}

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

static int seg_min(MinSeg *st, int node, int lo, int hi, int l, int r) {
    if (r < lo || hi < l) return INT_MAX;
    if (l <= lo && hi <= r) return st->tree[node];
    int mid = (lo + hi) / 2;
    return merge(seg_min(st, 2 * node, lo, mid, l, r),
                 seg_min(st, 2 * node + 1, mid + 1, hi, l, r));
}

int seg_query(MinSeg *st, int l, int r) {
    return seg_min(st, 1, 0, st->n - 1, l, r);
}
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;

class MinSegmentTree {
    static const int IDENTITY = INT_MAX;
    int n;
    vector<int> tree;

    int merge(int a, int b) { return min(a, b); }

    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] = merge(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] = merge(tree[2 * node], tree[2 * node + 1]);
    }

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

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

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

    int 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

Identical to the sum tree — the merge operation doesn't change the asymptotics as long as it's O(1).

When to use it

The operation must be associative — and mind the identity

Averages break the pattern: avg isn't associative, so store (sum, count) per node and divide at the end instead. Pick an identity that truly can't affect the result — using 0 as a min identity would silently make every query return 0. Non-invertible operations like min and max are exactly why a segment tree beats a Fenwick tree here: a BIT needs an inverse to subtract prefixes, which min doesn't have.

Practice

Recap

  • A segment tree answers any associative range query — sum, min, max, XOR, GCD — by swapping only the merge function and its identity.
  • A range decomposes into O(log n) canonical nodes; the identity keeps disjoint branches from corrupting the fold.
  • Bundle several aggregates in one node (min+max, value+index) to answer richer questions in a single pass.

How is this guide?

Last updated on

On this page