Stage 5: Interview Simulation·Rust-Specific Practice
Practice10 min readSegment TreeGenericsDesign

Rewrite: Segment Tree with generics

Generic iterative segment tree over a Monoid trait

Build a SegTree<T: Monoid> that works for sum, min, max, and GCD.

Published Apr 17, 2026

Exercise

Re-solve "range sum with point updates" using a generic SegTree<T: Monoid> in Rust, so the same tree answers sum / min / max / GCD queries by swapping the monoid. In TypeScript, build the equivalent with a factory that takes (identity, combine).

This problem isn't on LeetCode — it's a template-building exercise. Systems interviews often ask "implement a segment tree, then switch it from sum to max in one line." The goal is to produce a reusable primitive you'd drop into any competitive-programming repo or production scheduler.

Intuition

A segment tree is a binary tree where each leaf stores one element and each internal node stores the fold of its children under some associative operation. The fold is abstract — sum, min, max, GCD, matrix multiply — as long as it obeys the monoid laws:

  • Associative: op(a, op(b, c)) == op(op(a, b), c)
  • Identity: op(id(), x) == op(x, id()) == x

Make the element type a trait with id() and op() methods, and one segment tree serves every monoid. This is how crates.io/segment_tree, tomoprime/SegmentTree, and similar libraries ship in practice.

Iterative (bottom-up) layout places leaves at positions [n, 2n) and internal nodes at [1, n), with parent(i) = i >> 1, left(i) = 2i, right(i) = 2i + 1. No recursion, no stack blow-up.

Approach

Iterative Segment Tree over a Monoid

TimeO(log n) per op
SpaceO(n)
Recommended

Rust: declare trait Monoid: Clone { fn id() -> Self; fn op(a: &Self, b: &Self) -> Self; }. Store Vec<T> of length 2n where n is the next power of two ≥ input length. Bottom-up build: fill leaves, then for i from n - 1 down to 1, combine children. Point update: set leaf, then bubble up. Range query on [l, r): walk inward from both ends, accumulating on the left into acc_l when the boundary is a right child, and on the right into acc_r when the boundary is a left child.

TypeScript: same algorithm, but use a factory returning { update, query } because TS doesn't have traits. The identity value and combine function are captured by closure.

pub trait Monoid: Clone {
    fn id() -> Self;
    fn op(a: &Self, b: &Self) -> Self;
}

pub struct SegTree<T: Monoid> {
    n: usize,
    tree: Vec<T>,
}

impl<T: Monoid> SegTree<T> {
    pub fn from_vec(values: Vec<T>) -> Self {
        let len = values.len();
        let mut n = 1;
        while n < len.max(1) { n <<= 1; }
        let mut tree= vec![T::id(); 2 * n];
        for (i, v) in values.into_iter().enumerate() {
            tree[n + i]= v;
        }
        for i in (1..n).rev() {
            tree[i]= T::op(&tree[2 * i], &tree[2 * i + 1]);
        }
        SegTree { n, tree }
    }

    pub fn update(&mut self, i: usize, v: T) {
        let mut i= i + self.n;
        self.tree[i]= v;
        i >>= 1;
        while i > 0 {
            self.tree[i] = T::op(&self.tree[2 * i], &self.tree[2 * i + 1]);
            i >>= 1;
        }
    }

    /// Query on half-open range [l, r).
    pub fn query(&self, l: usize, r: usize) -> T {
        let mut acc_l = T::id();
        let mut acc_r = T::id();
        let (mut l, mut r) = (l + self.n, r + self.n);
        while l < r {
            if l & 1= 1 { acc_l= T::op(&acc_l, &self.tree[l]); l = 1; }
            if r & 1= 1 { r -= 1; acc_r= T::op(&self.tree[r], &acc_r); }
            l >>= 1;
            r >>= 1;
        }
        T::op(&acc_l, &acc_r)
    }
}
Rust · runnable

Trace

Building a sum-monoid tree over [1, 2, 3, 4] (n = 4):

indexrolestored
4..8leaves1 2 3 4
3node7 (=3 + 4)
2node3 (=1 + 2)
1root10 (=3 + 7)

Query [1, 3) walks (l=5, r=7) → take tree[5] = 2 on the left (odd), take tree[6] = 3 on the right (odd, r becomes 6 then decrements to 5) → acc = 2 + 3 = 5. Correct: 1 + 2 prefix excluded, 3 + 4 sum isn't — we got 2 + 3 = elements at indices 1 and 2.

Edge cases

  • Empty inputfrom_vec(vec![]) still builds a tree with n = 1 and a single identity leaf; query(0, 0) returns id().
  • Non-power-of-two size — pad up to the next power of two. Leaves past the input are initialized to id() so they never affect queries.
  • Single-element queryquery(i, i + 1) reduces to walking directly to leaf n + i and returning it.
  • Identity must be a true identity — e.g., for max, the identity is i64::MIN, not 0; getting this wrong silently returns 0 on empty ranges.
💡
Tip

The same tree supports every monoid by parametrizing T. Change the Monoid impl (or the combine closure in TS) and you get a min tree, a max tree, or a GCD tree for free. That reuse is the whole point of the trait-bound design.

Takeaway

Parametrize over the algebra. A segment tree is logically one data structure parametrized by the associative operation. Writing it once, generically, gives you a toolbox primitive — and writing it iteratively (no recursion) keeps it fast and stack-safe.

For Rust interviews, the Monoid trait pattern signals that you think in terms of algebraic abstractions, not hand-rolled per-problem implementations. That's the difference between "can solve segment tree problems" and "understands why segment trees work."

More in Rust-Specific Practice