Back to Technology

Segment Tree Variants: Iterative, Lazy Propagation & Beats

September 9, 2026 Wasil Zafar 38 min read

The recursive point-update segment tree and its persistent cousin are only the beginning. This lesson covers the six variants that show up once real workloads demand range updates, adversarial pruning, offline rank queries, sparse coordinates, or a second dimension.

Table of Contents

  1. Introduction & Motivation
  2. Prerequisites
  3. Iterative (Bottom-Up) Segment Tree
  4. Lazy Propagation
  5. Segment Tree Beats
  6. Merge-Sort Tree
  7. Dynamic / Implicit Segment Tree
  8. 2D Segment Tree
  9. Complexity Comparison
  10. Applications & Alternatives
  11. Exercises
  12. Summary Cheat Sheet
Mastery target: Level B implementation, progressing toward Level C fluency. You should be able to choose the right segment tree variant for a workload, implement it from scratch, and explain precisely why a plain recursive segment tree cannot serve that workload efficiently.

1. Introduction & Motivation

The recursive point-update, range-sum segment tree is the "hello world" of range data structures: build once, query and update in $O(\log n)$, done. But real workloads rarely stop there. A leaderboard needs to add a bonus to every player in a rank range, not just one player. A game server needs to cap thousands of values at once without touching every one of them individually. An offline analytics query needs to count how many values in a subarray fall below a threshold. A coordinate can be a 64-bit timestamp, making a dense $O(n)$ array impossible to allocate. A grid needs range sums over both rows and columns at once.

Each of these problems is answered by a specific segment tree variant, not by forcing the plain recursive version to do something it was never designed for. This lesson is a tour of six such variants: an iterative array-based rewrite that removes recursion overhead, lazy propagation for range updates, Segment Tree Beats for range chmin combined with range sum, the merge-sort tree for offline rank queries, a dynamic/implicit tree for sparse coordinate domains, and a 2D segment tree for grid queries. Every implementation here is a complete, traced-through program — not a sketch.

2. Prerequisites

This lesson assumes you are already comfortable with two structures covered elsewhere in this series and does not re-derive either of them:

If either of those two implementations is unfamiliar, read them first. Everything below assumes you already know how a segment tree recurses, and it deliberately covers only the variants that are genuinely missing from the rest of the series: iterative rewrites, lazy propagation, Segment Tree Beats, the merge-sort tree, dynamic/implicit trees, and 2D segment trees.

3. Iterative (Bottom-Up) Segment Tree

Core insight: Store the tree as a flat array of size $2n$: leaves occupy indices $n$ through $2n-1$ in original array order, and every internal node at index $i$ has children at $2i$ and $2i+1$ (equivalently, parent of $i$ is $\lfloor i/2 \rfloor$). Because the structure is fixed at construction time, update and query become plain iterative loops that walk up (update) or converge inward (query) — no recursion, no per-call stack frame, no $[start, end]$ bookkeeping passed down the call chain.

The tradeoff is a less flexible query interface: this classic construction answers range sum over a half-open interval $[l, r)$ rather than the closed $[l, r]$ interval the recursive version uses, and it is naturally suited to associative, invertible-free operations like sum, min, or max — not to arbitrary lazy range updates (that still needs the recursive form with lazy propagation, covered next). For raw point-update/range-query throughput, though, the flat-array iterative form is typically 2-3x faster in practice than the recursive version because it avoids function-call overhead entirely.

class IterativeSegmentTree:
    """
    Flat-array, bottom-up segment tree for range sum with point updates.
    Leaves live at indices [n, 2n); node i's children are 2*i and 2*i+1.
    Build: O(n). Update: O(log n). Query: O(log n). Range is half-open [l, r).
    """
    def __init__(self, nums):
        self.n = len(nums)
        self.tree = [0] * (2 * self.n)
        for i, v in enumerate(nums):
            self.tree[self.n + i] = v
        for i in range(self.n - 1, 0, -1):
            self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]

    def update(self, pos, value):
        """Point update: set index pos to value. O(log n), no recursion."""
        pos += self.n
        self.tree[pos] = value
        pos //= 2
        while pos >= 1:
            self.tree[pos] = self.tree[2 * pos] + self.tree[2 * pos + 1]
            pos //= 2

    def query(self, l, r):
        """Sum over half-open [l, r). O(log n), no recursion."""
        res = 0
        l += self.n
        r += self.n
        while l < r:
            if l & 1:
                res += self.tree[l]
                l += 1
            if r & 1:
                r -= 1
                res += self.tree[r]
            l //= 2
            r //= 2
        return res


st = IterativeSegmentTree([1, 3, 5, 7, 9, 11])
print(st.query(1, 4))   # 15 = 3 + 5 + 7  (elements at indices 1, 2, 3)
st.update(1, 10)         # set index 1 to 10
print(st.query(1, 4))   # 22 = 10 + 5 + 7
// Iterative (bottom-up) Segment Tree -- range sum, point update
// Build: O(n), Update: O(log n), Query: O(log n). Range is half-open [l, r).
#include <iostream>
#include <vector>
using namespace std;

class IterativeSegmentTree {
    vector<long long> tree;
    int n;
public:
    IterativeSegmentTree(const vector<int>& nums) {
        n = nums.size();
        tree.assign(2 * n, 0);
        for (int i = 0; i < n; i++) tree[n + i] = nums[i];
        for (int i = n - 1; i >= 1; i--) tree[i] = tree[2*i] + tree[2*i+1];
    }

    void update(int pos, int value) {
        pos += n;
        tree[pos] = value;
        for (pos /= 2; pos >= 1; pos /= 2)
            tree[pos] = tree[2*pos] + tree[2*pos+1];
    }

    long long query(int l, int r) {  // sum over half-open [l, r)
        long long res = 0;
        for (l += n, r += n; l < r; l /= 2, r /= 2) {
            if (l & 1) res += tree[l++];
            if (r & 1) res += tree[--r];
        }
        return res;
    }
};

int main() {
    vector<int> nums = {1, 3, 5, 7, 9, 11};
    IterativeSegmentTree st(nums);
    cout << st.query(1, 4) << endl;  // 15 (3+5+7)
    st.update(1, 10);
    cout << st.query(1, 4) << endl;  // 22 (10+5+7)
}
// Iterative (bottom-up) Segment Tree -- range sum, point update
// Build: O(n), Update: O(log n), Query: O(log n). Range is half-open [l, r).
public class IterativeSegmentTree {
    long[] tree;
    int n;

    IterativeSegmentTree(int[] nums) {
        n = nums.length;
        tree = new long[2 * n];
        for (int i = 0; i < n; i++) tree[n + i] = nums[i];
        for (int i = n - 1; i >= 1; i--) tree[i] = tree[2*i] + tree[2*i+1];
    }

    void update(int pos, int value) {
        pos += n;
        tree[pos] = value;
        for (pos /= 2; pos >= 1; pos /= 2)
            tree[pos] = tree[2*pos] + tree[2*pos+1];
    }

    long query(int l, int r) {  // sum over half-open [l, r)
        long res = 0;
        for (l += n, r += n; l < r; l /= 2, r /= 2) {
            if ((l & 1) == 1) res += tree[l++];
            if ((r & 1) == 1) res += tree[--r];
        }
        return res;
    }

    public static void main(String[] args) {
        int[] nums = {1, 3, 5, 7, 9, 11};
        IterativeSegmentTree st = new IterativeSegmentTree(nums);
        System.out.println(st.query(1, 4));  // 15 (3+5+7)
        st.update(1, 10);
        System.out.println(st.query(1, 4));  // 22 (10+5+7)
    }
}

Tracing the flat array for [1, 3, 5, 7, 9, 11] (so $n=6$, tree size $12$) after the initial build:

IndexValueRole
0—unused (indices start at 1)
136root: sum of the whole array
232internal: sum of leaves 8, 9, 10, 11
34internal: sum of leaves 6, 7
412internal: sum of leaves 8, 9
520internal: sum of leaves 10, 11
61leaf: array[0]
73leaf: array[1]
85leaf: array[2]
97leaf: array[3]
109leaf: array[4]
1111leaf: array[5]

Because $n=6$ is not a power of two, the internal ranges are not perfectly balanced left and right (node 2 covers four leaves while node 3 covers two) — this is expected and does not affect correctness, only the assumption that every level is "full." query(1, 4) walks $l=7, r=10$ inward: it adds tree[7]=3 (since $l$ is odd), then tree[4]=12 (since, after halving, the new $r=5$ is odd), for a total of $15$, matching $3+5+7$.

4. Lazy Propagation

Core insight: When a range update fully covers a node's range, apply its effect to that node's own aggregate immediately, but defer pushing it down to the node's children by stashing it in a lazy slot. Only "flush" that pending value into the children when a later query or update actually needs to descend past this node. This is what turns a range update from $O(n)$ (touching every affected leaf) into $O(\log n)$ (touching only the $O(\log n)$ nodes whose ranges are fully covered, plus the $O(\log n)$ nodes on the path down to them).

Without laziness, a range-add segment tree would need to update every leaf in the range, which is $O(n)$ in the worst case — no better than a plain array. Lazy propagation is the single idea that makes range updates as cheap as range queries.

class LazySegmentTree:
    """
    Range-add / range-sum segment tree with lazy propagation.
    A pending add on a node is applied to that node's own sum right away,
    but only pushed down into its children when a query or update needs
    to descend past it. Update: O(log n). Query: O(log n).
    """
    def __init__(self, arr):
        self.n = len(arr)
        size = 4 * self.n
        self.tree = [0] * size
        self.lazy = [0] * size
        self._build(arr, 1, 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, lo, mid)
        self._build(arr, 2 * node + 1, mid + 1, hi)
        self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

    def _push_down(self, node, lo, hi):
        if self.lazy[node] == 0 or lo == hi:
            return
        mid = (lo + hi) // 2
        for child, child_lo, child_hi in ((2 * node, lo, mid), (2 * node + 1, mid + 1, hi)):
            self.tree[child] += self.lazy[node] * (child_hi - child_lo + 1)
            self.lazy[child] += self.lazy[node]
        self.lazy[node] = 0

    def update_range(self, l, r, val, node=1, lo=0, hi=None):
        if hi is None:
            hi = 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
            return
        self._push_down(node, lo, hi)
        mid = (lo + hi) // 2
        self.update_range(l, r, val, 2 * node, lo, mid)
        self.update_range(l, r, val, 2 * node + 1, mid + 1, hi)
        self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

    def query_range(self, l, r, node=1, lo=0, hi=None):
        if hi is None:
            hi = 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)
        mid = (lo + hi) // 2
        return (self.query_range(l, r, 2 * node, lo, mid)
              + self.query_range(l, r, 2 * node + 1, mid + 1, hi))


arr = [1, 3, 5, 7, 9, 11]
lst = LazySegmentTree(arr)
lst.update_range(2, 4, 5)      # add 5 to indices 2..4 -> values become 10, 12, 14
print(lst.query_range(2, 4))  # 36  (10 + 12 + 14)
print(lst.query_range(3, 3))  # 12  (7 + 5); forces a push-down through the node covering [3,4]
// Lazy Propagation -- range add, range sum
// Update: O(log n), Query: O(log n)
#include <iostream>
#include <vector>
using namespace std;

class LazySegmentTree {
    vector<long long> tree, lazy;
    int n;

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

    void pushDown(int node, int lo, int hi) {
        if (lazy[node] == 0 || lo == hi) return;
        int mid = (lo + hi) / 2;
        int left = 2*node, right = 2*node+1;
        tree[left]  += lazy[node] * (mid - lo + 1);
        tree[right] += lazy[node] * (hi - mid);
        lazy[left]  += lazy[node];
        lazy[right] += lazy[node];
        lazy[node] = 0;
    }

public:
    LazySegmentTree(const vector<int>& arr) {
        n = arr.size();
        tree.assign(4 * n, 0);
        lazy.assign(4 * n, 0);
        build(arr, 1, 0, n - 1);
    }

    void updateRange(int l, int r, int val, int node = 1, int lo = 0, int hi = -1) {
        if (hi == -1) hi = n - 1;
        if (r < lo || hi < l) return;
        if (l <= lo && hi <= r) {
            tree[node] += (long long) val * (hi - lo + 1);
            lazy[node] += val;
            return;
        }
        pushDown(node, lo, hi);
        int mid = (lo + hi) / 2;
        updateRange(l, r, val, 2*node, lo, mid);
        updateRange(l, r, val, 2*node+1, mid+1, hi);
        tree[node] = tree[2*node] + tree[2*node+1];
    }

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

int main() {
    vector<int> arr = {1, 3, 5, 7, 9, 11};
    LazySegmentTree lst(arr);
    lst.updateRange(2, 4, 5);
    cout << lst.queryRange(2, 4) << endl;  // 36
    cout << lst.queryRange(3, 3) << endl;  // 12
}
// Lazy Propagation -- range add, range sum
// Update: O(log n), Query: O(log n)
public class LazySegmentTree {
    long[] tree, lazy;
    int n;

    LazySegmentTree(int[] arr) {
        n = arr.length;
        tree = new long[4 * n];
        lazy = new long[4 * n];
        build(arr, 1, 0, n - 1);
    }

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

    void pushDown(int node, int lo, int hi) {
        if (lazy[node] == 0 || lo == hi) return;
        int mid = (lo + hi) / 2;
        int left = 2*node, right = 2*node+1;
        tree[left]  += lazy[node] * (mid - lo + 1);
        tree[right] += lazy[node] * (hi - mid);
        lazy[left]  += lazy[node];
        lazy[right] += lazy[node];
        lazy[node] = 0;
    }

    void updateRange(int l, int r, int val, int node, int lo, int hi) {
        if (r < lo || hi < l) return;
        if (l <= lo && hi <= r) {
            tree[node] += (long) val * (hi - lo + 1);
            lazy[node] += val;
            return;
        }
        pushDown(node, lo, hi);
        int mid = (lo + hi) / 2;
        updateRange(l, r, val, 2*node, lo, mid);
        updateRange(l, r, val, 2*node+1, mid+1, hi);
        tree[node] = tree[2*node] + tree[2*node+1];
    }

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

    public static void main(String[] args) {
        int[] arr = {1, 3, 5, 7, 9, 11};
        LazySegmentTree lst = new LazySegmentTree(arr);
        lst.updateRange(2, 4, 5, 1, 0, arr.length - 1);
        System.out.println(lst.queryRange(2, 4, 1, 0, arr.length - 1));  // 36
        System.out.println(lst.queryRange(3, 3, 1, 0, arr.length - 1));  // 12
    }
}

Tracing update_range(2, 4, 5) then query_range(3, 3) against [1, 3, 5, 7, 9, 11]:

StepNode (range)What happenstree[node]lazy[node]
1node covering [2,2]fully inside [2,4]: apply directly5 → 100 → 5
2node covering [3,4]fully inside [2,4]: apply directly (children untouched)16 → 260 → 5
3root [0,5]recomputed from children after both branches return36 → 510
4query(3,3) descends into node [3,4]not fully covered by [3,3]: must push down first26 (unchanged)5 → 0
5leaf [3,3] (after push-down)received the deferred +57 → 120
6leaf [4,4] (after push-down)received the deferred +5 too, even though not queried9 → 140

Note step 2: the node covering [3,4] updates its own sum to 26 immediately, but its two children (values 7 and 9) are untouched until something forces a descent past that node — which is exactly what query_range(3, 3) does in step 4, since a single-index query cannot be satisfied by a node spanning two indices.

5. Segment Tree Beats

Core insight: Segment Tree Beats answers "range chmin" — for every index in $[l, r]$, set $arr[i] = \min(arr[i], x)$ — together with range-sum queries. Naively this needs $O(n)$ work per update, since a chmin can affect an arbitrary subset of elements and a plain lazy tag can't represent "each element becomes the smaller of itself and $x$." The trick: each node tracks its maximum value ($max_1$), the largest value strictly less than the maximum ($max_2$), and the count of elements equal to the maximum ($cnt$). A chmin update by $x$ then has three cases at any node: if $x \ge max_1$, the update changes nothing here — skip the whole subtree; if $max_2 \le x < max_1$, only the elements equal to $max_1$ are affected, and since there are exactly $cnt$ of them, the node's sum can be corrected in $O(1)$ without recursing further; only when $x < max_2$ (the update reaches into the "second tier" of values) must the algorithm recurse into children.

Most subtrees are skipped entirely or updated in $O(1)$; only a bounded number of nodes per update actually need to recurse. The amortized cost per chmin update is $O(\log^2 n)$ — this is proven with a potential-function argument (the number of distinct values in the array is a potential that never increases and strictly decreases every time the algorithm is forced to recurse past the trivial case), first shown by Ji Driver / the "HDU 5306" line of results. That proof is not repeated in full here; the important takeaway is that the pruning conditions above (skip when $x \ge max_1$, apply in $O(1)$ when $max_2 \le x < max_1$) are exactly what makes the amortized bound hold — get either condition wrong and the structure degrades to $O(n)$ per update.

class SegmentTreeBeats:
    """
    Segment Tree Beats: range chmin(l, r, x) sets arr[i] = min(arr[i], x)
    for every i in [l, r]; range_sum(l, r) sums the current values.
    Each node tracks max1 (largest value), max2 (largest value strictly
    below max1), cnt (how many elements equal max1), and sum. A chmin
    update either skips a subtree (x >= max1), applies in O(1) to just
    the max1 group (max2 <= x < max1), or recurses (x < max2).
    Amortized update: O(log^2 n). Query: O(log n).
    """
    NEG_INF = float('-inf')
    INF = float('inf')

    def __init__(self, arr):
        self.n = len(arr)
        size = 4 * self.n
        self.max1 = [0] * size
        self.max2 = [self.NEG_INF] * size
        self.cnt = [0] * size
        self.sum = [0] * size
        self.tag = [self.INF] * size
        self._build(arr, 1, 0, self.n - 1)

    def _build(self, arr, node, lo, hi):
        if lo == hi:
            self.max1[node] = arr[lo]
            self.max2[node] = self.NEG_INF
            self.cnt[node] = 1
            self.sum[node] = arr[lo]
            return
        mid = (lo + hi) // 2
        self._build(arr, 2 * node, lo, mid)
        self._build(arr, 2 * node + 1, mid + 1, hi)
        self._pull(node)

    def _pull(self, node):
        l, r = 2 * node, 2 * node + 1
        self.sum[node] = self.sum[l] + self.sum[r]
        if self.max1[l] == self.max1[r]:
            self.max1[node] = self.max1[l]
            self.cnt[node] = self.cnt[l] + self.cnt[r]
            self.max2[node] = max(self.max2[l], self.max2[r])
        elif self.max1[l] > self.max1[r]:
            self.max1[node] = self.max1[l]
            self.cnt[node] = self.cnt[l]
            self.max2[node] = max(self.max2[l], self.max1[r])
        else:
            self.max1[node] = self.max1[r]
            self.cnt[node] = self.cnt[r]
            self.max2[node] = max(self.max1[l], self.max2[r])

    def _apply(self, node, x):
        """Apply chmin(x) to this node's max1 group only.
        Caller guarantees max2 <= x < max1 (or a no-op if x >= max1)."""
        if x >= self.max1[node]:
            return
        self.sum[node] -= (self.max1[node] - x) * self.cnt[node]
        self.max1[node] = x
        self.tag[node] = min(self.tag[node], x)

    def _push_down(self, node, lo, hi):
        if lo == hi or self.tag[node] == self.INF:
            return
        self._apply(2 * node, self.tag[node])
        self._apply(2 * node + 1, self.tag[node])
        self.tag[node] = self.INF

    def update_chmin(self, l, r, x, node=1, lo=0, hi=None):
        if hi is None:
            hi = self.n - 1
        if r < lo or hi < l or x >= self.max1[node]:
            return
        if l <= lo and hi <= r and x > self.max2[node]:
            self._apply(node, x)
            return
        self._push_down(node, lo, hi)
        mid = (lo + hi) // 2
        self.update_chmin(l, r, x, 2 * node, lo, mid)
        self.update_chmin(l, r, x, 2 * node + 1, mid + 1, hi)
        self._pull(node)

    def query_sum(self, l, r, node=1, lo=0, hi=None):
        if hi is None:
            hi = self.n - 1
        if r < lo or hi < l:
            return 0
        if l <= lo and hi <= r:
            return self.sum[node]
        self._push_down(node, lo, hi)
        mid = (lo + hi) // 2
        return (self.query_sum(l, r, 2 * node, lo, mid)
              + self.query_sum(l, r, 2 * node + 1, mid + 1, hi))


arr = [5, 1, 4, 3, 2]
st = SegmentTreeBeats(arr)
print(st.query_sum(0, 4))     # 15
st.update_chmin(0, 4, 3)      # arr becomes [3, 1, 3, 3, 2]
print(st.query_sum(0, 4))     # 12 = 3 + 1 + 3 + 3 + 2
print(st.query_sum(1, 1))     # 1  (untouched: 1 < 3 already, never rewritten)
// Segment Tree Beats -- range chmin, range sum
// Amortized update: O(log^2 n). Query: O(log n).
#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;

class SegmentTreeBeats {
    int n;
    vector<long long> max1, max2, sum, tag;
    vector<int> cnt;
    static const long long NEG_INF = LLONG_MIN;
    static const long long NO_TAG = LLONG_MAX;

    void pull(int node) {
        int l = 2*node, r = 2*node+1;
        sum[node] = sum[l] + sum[r];
        if (max1[l] == max1[r]) {
            max1[node] = max1[l];
            cnt[node] = cnt[l] + cnt[r];
            max2[node] = max(max2[l], max2[r]);
        } else if (max1[l] > max1[r]) {
            max1[node] = max1[l];
            cnt[node] = cnt[l];
            max2[node] = max(max2[l], max1[r]);
        } else {
            max1[node] = max1[r];
            cnt[node] = cnt[r];
            max2[node] = max(max1[l], max2[r]);
        }
    }

    void apply(int node, long long x) {
        if (x >= max1[node]) return;
        sum[node] -= (max1[node] - x) * cnt[node];
        max1[node] = x;
        tag[node] = min(tag[node], x);
    }

    void pushDown(int node, int lo, int hi) {
        if (lo == hi || tag[node] == NO_TAG) return;
        apply(2*node, tag[node]);
        apply(2*node+1, tag[node]);
        tag[node] = NO_TAG;
    }

    void build(const vector<int>& arr, int node, int lo, int hi) {
        if (lo == hi) {
            max1[node] = arr[lo];
            max2[node] = NEG_INF;
            cnt[node] = 1;
            sum[node] = arr[lo];
            return;
        }
        int mid = (lo + hi) / 2;
        build(arr, 2*node, lo, mid);
        build(arr, 2*node+1, mid+1, hi);
        pull(node);
    }

public:
    SegmentTreeBeats(const vector<int>& arr) {
        n = arr.size();
        int size = 4 * n;
        max1.assign(size, 0); max2.assign(size, NEG_INF);
        cnt.assign(size, 0); sum.assign(size, 0);
        tag.assign(size, NO_TAG);
        build(arr, 1, 0, n - 1);
    }

    void updateChmin(int l, int r, long long x, int node = 1, int lo = 0, int hi = -1) {
        if (hi == -1) hi = n - 1;
        if (r < lo || hi < l || x >= max1[node]) return;
        if (l <= lo && hi <= r && x > max2[node]) {
            apply(node, x);
            return;
        }
        pushDown(node, lo, hi);
        int mid = (lo + hi) / 2;
        updateChmin(l, r, x, 2*node, lo, mid);
        updateChmin(l, r, x, 2*node+1, mid+1, hi);
        pull(node);
    }

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

int main() {
    vector<int> arr = {5, 1, 4, 3, 2};
    SegmentTreeBeats st(arr);
    cout << st.querySum(0, 4) << endl;   // 15
    st.updateChmin(0, 4, 3);
    cout << st.querySum(0, 4) << endl;   // 12
    cout << st.querySum(1, 1) << endl;   // 1
}
// Segment Tree Beats -- range chmin, range sum
// Amortized update: O(log^2 n). Query: O(log n).
import java.util.Arrays;

public class SegmentTreeBeats {
    int n;
    long[] max1, sum, max2, tag;
    int[] cnt;
    static final long NEG_INF = Long.MIN_VALUE;
    static final long NO_TAG = Long.MAX_VALUE;

    SegmentTreeBeats(int[] arr) {
        n = arr.length;
        int size = 4 * n;
        max1 = new long[size];
        max2 = new long[size];
        cnt = new int[size];
        sum = new long[size];
        tag = new long[size];
        Arrays.fill(max2, NEG_INF);
        Arrays.fill(tag, NO_TAG);
        build(arr, 1, 0, n - 1);
    }

    void build(int[] arr, int node, int lo, int hi) {
        if (lo == hi) {
            max1[node] = arr[lo];
            max2[node] = NEG_INF;
            cnt[node] = 1;
            sum[node] = arr[lo];
            return;
        }
        int mid = (lo + hi) / 2;
        build(arr, 2*node, lo, mid);
        build(arr, 2*node+1, mid+1, hi);
        pull(node);
    }

    void pull(int node) {
        int l = 2*node, r = 2*node+1;
        sum[node] = sum[l] + sum[r];
        if (max1[l] == max1[r]) {
            max1[node] = max1[l];
            cnt[node] = cnt[l] + cnt[r];
            max2[node] = Math.max(max2[l], max2[r]);
        } else if (max1[l] > max1[r]) {
            max1[node] = max1[l];
            cnt[node] = cnt[l];
            max2[node] = Math.max(max2[l], max1[r]);
        } else {
            max1[node] = max1[r];
            cnt[node] = cnt[r];
            max2[node] = Math.max(max1[l], max2[r]);
        }
    }

    void apply(int node, long x) {
        if (x >= max1[node]) return;
        sum[node] -= (max1[node] - x) * cnt[node];
        max1[node] = x;
        tag[node] = Math.min(tag[node], x);
    }

    void pushDown(int node, int lo, int hi) {
        if (lo == hi || tag[node] == NO_TAG) return;
        apply(2*node, tag[node]);
        apply(2*node+1, tag[node]);
        tag[node] = NO_TAG;
    }

    void updateChmin(int l, int r, long x, int node, int lo, int hi) {
        if (r < lo || hi < l || x >= max1[node]) return;
        if (l <= lo && hi <= r && x > max2[node]) {
            apply(node, x);
            return;
        }
        pushDown(node, lo, hi);
        int mid = (lo + hi) / 2;
        updateChmin(l, r, x, 2*node, lo, mid);
        updateChmin(l, r, x, 2*node+1, mid+1, hi);
        pull(node);
    }

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

    public static void main(String[] args) {
        int[] arr = {5, 1, 4, 3, 2};
        SegmentTreeBeats st = new SegmentTreeBeats(arr);
        int last = arr.length - 1;
        System.out.println(st.querySum(0, 4, 1, 0, last));   // 15
        st.updateChmin(0, 4, 3, 1, 0, last);
        System.out.println(st.querySum(0, 4, 1, 0, last));   // 12
        System.out.println(st.querySum(1, 1, 1, 0, last));   // 1
    }
}

Tracing update_chmin(0, 4, 3) against [5, 1, 4, 3, 2] (initial sum 15): the node covering index 0 alone has $max_1=5$, $max_2=-\infty$, so $x=3 > max_2$ — it applies directly, sum drops by $(5-3) \times 1 = 2$. The node covering indices 3-4 has $max_1=3$, and since $x=3 \ge max_1$, it is skipped entirely with no recursion at all. The node covering index 1 has $max_1=1$, and since $x=3 \ge max_1$, it too is skipped — index 1's value of 1 is never touched, which is exactly why query_sum(1, 1) still returns 1 afterward. Final sum: $3+1+3+3+2=12$, matching the traced result above.

6. Merge-Sort Tree

Core insight: Build a segment tree exactly like the recursive version, except each node stores a sorted copy of the subarray it represents, built by merging its two children's sorted arrays — literally the merge step of merge sort, applied once per node instead of once per array. To answer "how many elements in $[l, r]$ are $\le x$," decompose the query into $O(\log n)$ nodes as usual, and binary-search each node's sorted array for the count $\le x$ in $O(\log n)$ — giving $O(\log^2 n)$ total. This structure has no updates in its basic form; it trades update support for fast offline rank/count queries.
class MergeSortTree:
    """
    Each node stores a sorted copy of its subarray, built merge-sort style.
    count_leq(l, r, x): how many elements in [l, r] are <= x. O(log^2 n).
    Build: O(n log n).
    """
    def __init__(self, arr):
        self.n = len(arr)
        self.tree = [[] for _ in range(4 * self.n)]
        self._build(arr, 1, 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, lo, mid)
        self._build(arr, 2 * node + 1, mid + 1, hi)
        self.tree[node] = self._merge(self.tree[2 * node], self.tree[2 * node + 1])

    @staticmethod
    def _merge(a, b):
        merged, i, j = [], 0, 0
        while i < len(a) and j < len(b):
            if a[i] <= b[j]:
                merged.append(a[i]); i += 1
            else:
                merged.append(b[j]); j += 1
        merged.extend(a[i:])
        merged.extend(b[j:])
        return merged

    @staticmethod
    def _upper_bound(sorted_list, x):
        lo, hi = 0, len(sorted_list)
        while lo < hi:
            mid = (lo + hi) // 2
            if sorted_list[mid] <= x:
                lo = mid + 1
            else:
                hi = mid
        return lo

    def count_leq(self, l, r, x, node=1, lo=0, hi=None):
        if hi is None:
            hi = self.n - 1
        if r < lo or hi < l:
            return 0
        if l <= lo and hi <= r:
            return self._upper_bound(self.tree[node], x)
        mid = (lo + hi) // 2
        return (self.count_leq(l, r, x, 2 * node, lo, mid)
              + self.count_leq(l, r, x, 2 * node + 1, mid + 1, hi))


arr = [5, 1, 4, 3, 2]
mst = MergeSortTree(arr)
print(mst.count_leq(0, 4, 3))  # 3  (1, 3, 2 are <= 3, out of the whole array)
print(mst.count_leq(1, 3, 3))  # 2  (subarray [1, 4, 3]: 1 and 3 are <= 3)
// Merge-Sort Tree -- count elements <= x in a range
// Build: O(n log n). Query: O(log^2 n).
#include <iostream>
#include <vector>
using namespace std;

class MergeSortTree {
    vector<vector<int>> tree;
    int n;

    vector<int> merge2(const vector<int>& a, const vector<int>& b) {
        vector<int> merged;
        merged.reserve(a.size() + b.size());
        size_t i = 0, j = 0;
        while (i < a.size() && j < b.size()) {
            if (a[i] <= b[j]) merged.push_back(a[i++]);
            else merged.push_back(b[j++]);
        }
        while (i < a.size()) merged.push_back(a[i++]);
        while (j < b.size()) merged.push_back(b[j++]);
        return merged;
    }

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

    int upperBound(const vector<int>& sorted, int x) {
        int lo = 0, hi = sorted.size();
        while (lo < hi) {
            int mid = (lo + hi) / 2;
            if (sorted[mid] <= x) lo = mid + 1;
            else hi = mid;
        }
        return lo;
    }

public:
    MergeSortTree(const vector<int>& arr) {
        n = arr.size();
        tree.resize(4 * n);
        build(arr, 1, 0, n - 1);
    }

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

int main() {
    vector<int> arr = {5, 1, 4, 3, 2};
    MergeSortTree mst(arr);
    cout << mst.countLeq(0, 4, 3) << endl;  // 3 (1, 3, 2)
    cout << mst.countLeq(1, 3, 3) << endl;  // 2 (1, 3)
}
// Merge-Sort Tree -- count elements <= x in a range
// Build: O(n log n). Query: O(log^2 n).
import java.util.ArrayList;
import java.util.List;

public class MergeSortTree {
    List<Integer>[] tree;
    int n;

    @SuppressWarnings("unchecked")
    MergeSortTree(int[] arr) {
        n = arr.length;
        tree = new List[4 * n];
        build(arr, 1, 0, n - 1);
    }

    void build(int[] arr, int node, int lo, int hi) {
        if (lo == hi) {
            List<Integer> single = new ArrayList<>();
            single.add(arr[lo]);
            tree[node] = single;
            return;
        }
        int mid = (lo + hi) / 2;
        build(arr, 2*node, lo, mid);
        build(arr, 2*node+1, mid+1, hi);
        tree[node] = merge(tree[2*node], tree[2*node+1]);
    }

    List<Integer> merge(List<Integer> a, List<Integer> b) {
        List<Integer> merged = new ArrayList<>(a.size() + b.size());
        int i = 0, j = 0;
        while (i < a.size() && j < b.size()) {
            if (a.get(i) <= b.get(j)) merged.add(a.get(i++));
            else merged.add(b.get(j++));
        }
        while (i < a.size()) merged.add(a.get(i++));
        while (j < b.size()) merged.add(b.get(j++));
        return merged;
    }

    int upperBound(List<Integer> sorted, int x) {
        int lo = 0, hi = sorted.size();
        while (lo < hi) {
            int mid = (lo + hi) / 2;
            if (sorted.get(mid) <= x) lo = mid + 1;
            else hi = mid;
        }
        return lo;
    }

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

    public static void main(String[] args) {
        int[] arr = {5, 1, 4, 3, 2};
        MergeSortTree mst = new MergeSortTree(arr);
        int last = arr.length - 1;
        System.out.println(mst.countLeq(0, 4, 3, 1, 0, last));  // 3
        System.out.println(mst.countLeq(1, 3, 3, 1, 0, last));  // 2
    }
}

For [5, 1, 4, 3, 2], the root's sorted array is [1, 2, 3, 4, 5] (the merge of every node down to the leaves), so count_leq(0, 4, 3) resolves entirely at the root: binary search finds 3 elements $\le 3$. count_leq(1, 3, 3) decomposes into the node covering index 1 (sorted [1], contributing 1) and the node covering indices 3-4 (sorted [2, 3], contributing 1 for the value 3) — total 2, matching the subarray [1, 4, 3] having exactly two values $\le 3$.

7. Dynamic / Implicit Segment Tree

Core insight: When the coordinate range is enormous or sparse (values up to $10^9$, say) but only a few thousand positions are ever actually updated, allocating an $O(\text{range})$ array up front is impossible. Instead, represent nodes as objects with explicit left/right child pointers (instead of array indices $2i, 2i+1$), and only create a child node the first time an update or query path actually reaches it. Untouched regions of the coordinate space never materialize any nodes at all — they implicitly contribute zero.

The time complexity per operation becomes $O(\log U)$ where $U$ is the size of the coordinate universe (not the number of elements actually present), and the space used is $O(Q \log U)$ for $Q$ updates — each update creates at most one new node per level of the tree.

class DynamicNode:
    __slots__ = ("sum", "left", "right")
    def __init__(self):
        self.sum = 0
        self.left = None
        self.right = None


class DynamicSegmentTree:
    """
    Point-update / range-sum segment tree over a huge coordinate range
    (e.g. [0, 10**9]) that never allocates O(range) nodes up front.
    A node is created only the first time an update actually touches it.
    Update: O(log U). Query: O(log U), where U is the coordinate range size.
    """
    def __init__(self, lo, hi):
        self.lo, self.hi = lo, hi
        self.root = DynamicNode()

    def update(self, idx, delta):
        self._update(self.root, self.lo, self.hi, idx, delta)

    def _update(self, node, lo, hi, idx, delta):
        node.sum += delta
        if lo == hi:
            return
        mid = (lo + hi) // 2
        if idx <= mid:
            if node.left is None:
                node.left = DynamicNode()
            self._update(node.left, lo, mid, idx, delta)
        else:
            if node.right is None:
                node.right = DynamicNode()
            self._update(node.right, mid + 1, hi, idx, delta)

    def query(self, l, r):
        return self._query(self.root, self.lo, self.hi, l, r)

    def _query(self, node, lo, hi, l, r):
        if node is None or r < lo or hi < l:
            return 0
        if l <= lo and hi <= r:
            return node.sum
        mid = (lo + hi) // 2
        return (self._query(node.left, lo, mid, l, r)
              + self._query(node.right, mid + 1, hi, l, r))


dst = DynamicSegmentTree(0, 10**9)
dst.update(7, 5)
dst.update(1_000_000_000, 3)
print(dst.query(0, 10))                  # 5
print(dst.query(0, 10**9))               # 8
print(dst.query(500_000_000, 10**9))     # 3
// Dynamic / Implicit Segment Tree -- point update, range sum
// Update: O(log U). Query: O(log U). Nodes created lazily.
#include <iostream>
using namespace std;

struct DynNode {
    long long sum = 0;
    DynNode *left = nullptr, *right = nullptr;
};

class DynamicSegmentTree {
    long long lo, hi;
    DynNode* root;

    void update(DynNode* node, long long lo, long long hi, long long idx, long long delta) {
        node->sum += delta;
        if (lo == hi) return;
        long long mid = lo + (hi - lo) / 2;
        if (idx <= mid) {
            if (!node->left) node->left = new DynNode();
            update(node->left, lo, mid, idx, delta);
        } else {
            if (!node->right) node->right = new DynNode();
            update(node->right, mid + 1, hi, idx, delta);
        }
    }

    long long query(DynNode* node, long long lo, long long hi, long long l, long long r) {
        if (!node || r < lo || hi < l) return 0;
        if (l <= lo && hi <= r) return node->sum;
        long long mid = lo + (hi - lo) / 2;
        return query(node->left, lo, mid, l, r) + query(node->right, mid + 1, hi, l, r);
    }

public:
    DynamicSegmentTree(long long lo_, long long hi_) : lo(lo_), hi(hi_) {
        root = new DynNode();
    }

    void update(long long idx, long long delta) { update(root, lo, hi, idx, delta); }
    long long query(long long l, long long r) { return query(root, lo, hi, l, r); }
};

int main() {
    DynamicSegmentTree dst(0, 1000000000LL);
    dst.update(7, 5);
    dst.update(1000000000LL, 3);
    cout << dst.query(0, 10) << endl;                     // 5
    cout << dst.query(0, 1000000000LL) << endl;            // 8
    cout << dst.query(500000000LL, 1000000000LL) << endl;  // 3
}
// Dynamic / Implicit Segment Tree -- point update, range sum
// Update: O(log U). Query: O(log U). Nodes created lazily.
public class DynamicSegmentTree {
    static class Node {
        long sum = 0;
        Node left, right;
    }

    long lo, hi;
    Node root;

    DynamicSegmentTree(long lo, long hi) {
        this.lo = lo;
        this.hi = hi;
        root = new Node();
    }

    public void update(long idx, long delta) { update(root, lo, hi, idx, delta); }

    private void update(Node node, long lo, long hi, long idx, long delta) {
        node.sum += delta;
        if (lo == hi) return;
        long mid = lo + (hi - lo) / 2;
        if (idx <= mid) {
            if (node.left == null) node.left = new Node();
            update(node.left, lo, mid, idx, delta);
        } else {
            if (node.right == null) node.right = new Node();
            update(node.right, mid + 1, hi, idx, delta);
        }
    }

    public long query(long l, long r) { return query(root, lo, hi, l, r); }

    private long query(Node node, long lo, long hi, long l, long r) {
        if (node == null || r < lo || hi < l) return 0;
        if (l <= lo && hi <= r) return node.sum;
        long mid = lo + (hi - lo) / 2;
        return query(node.left, lo, mid, l, r) + query(node.right, mid + 1, hi, l, r);
    }

    public static void main(String[] args) {
        DynamicSegmentTree dst = new DynamicSegmentTree(0, 1_000_000_000L);
        dst.update(7, 5);
        dst.update(1_000_000_000L, 3);
        System.out.println(dst.query(0, 10));                        // 5
        System.out.println(dst.query(0, 1_000_000_000L));            // 8
        System.out.println(dst.query(500_000_000L, 1_000_000_000L)); // 3
    }
}

Only two positions are ever updated here — index 7 and index $10^9$ — over a coordinate universe of a billion. update(7, 5) creates roughly $\log_2(10^9) \approx 30$ nodes along the path from the root to the leaf at 7; update(10^9, 3) creates another $\approx 30$ nodes down a completely different path. Everywhere else in the coordinate space, no node ever exists, and query treats a missing node as contributing zero without allocating anything.

8. 2D Segment Tree

A 2D segment tree answers range-sum (or range-min) queries over a rectangle of a grid. The idea: build an outer segment tree over rows, where every outer node — leaf or internal — holds a complete inner segment tree over columns, representing the column-wise sums of whatever row range that outer node covers. A point update touches one inner tree per outer ancestor ($O(\log R)$ of them), each update costing $O(\log C)$ — $O(\log R \cdot \log C)$ total. A rectangle query decomposes the outer range into $O(\log R)$ nodes and queries each one's inner tree over the column range, again $O(\log R \cdot \log C)$.

class SegTree1D:
    """Iterative point-add / range-sum tree, half-open [l, r). Reused as the inner tree."""
    def __init__(self, values):
        self.n = len(values)
        self.tree = [0] * (2 * self.n)
        for i, v in enumerate(values):
            self.tree[self.n + i] = v
        for i in range(self.n - 1, 0, -1):
            self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]

    def add(self, pos, delta):
        pos += self.n
        self.tree[pos] += delta
        pos //= 2
        while pos >= 1:
            self.tree[pos] = self.tree[2 * pos] + self.tree[2 * pos + 1]
            pos //= 2

    def range_sum(self, l, r):
        res = 0
        l += self.n
        r += self.n
        while l < r:
            if l & 1:
                res += self.tree[l]; l += 1
            if r & 1:
                r -= 1; res += self.tree[r]
            l //= 2; r //= 2
        return res


class SegmentTree2D:
    """
    Outer iterative segment tree over rows; every outer node holds an inner
    SegTree1D over columns for the row-range it represents. A point update
    touches one inner tree per outer ancestor (O(log R) of them), each in
    O(log C) -- O(log R * log C) total. Queries decompose the same way.
    """
    def __init__(self, grid):
        self.rows = len(grid)
        self.cols = len(grid[0]) if grid else 0
        self.grid = [row[:] for row in grid]
        self.outer = [None] * (2 * self.rows)
        for r in range(self.rows):
            self.outer[self.rows + r] = SegTree1D(grid[r])
        for r in range(self.rows - 1, 0, -1):
            left, right = self.outer[2 * r], self.outer[2 * r + 1]
            merged = [left.range_sum(c, c + 1) + right.range_sum(c, c + 1) for c in range(self.cols)]
            self.outer[r] = SegTree1D(merged)

    def update(self, r, c, val):
        delta = val - self.grid[r][c]
        self.grid[r][c] = val
        pos = self.rows + r
        self.outer[pos].add(c, delta)
        pos //= 2
        while pos >= 1:
            self.outer[pos].add(c, delta)
            pos //= 2

    def query(self, r1, r2, c1, c2):
        """Sum over rows [r1, r2) and columns [c1, c2)."""
        total = 0
        l, r = r1 + self.rows, r2 + self.rows
        while l < r:
            if l & 1:
                total += self.outer[l].range_sum(c1, c2); l += 1
            if r & 1:
                r -= 1; total += self.outer[r].range_sum(c1, c2)
            l //= 2; r //= 2
        return total


grid = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]
st2d = SegmentTree2D(grid)
print(st2d.query(0, 3, 0, 3))   # 45 (sum of all 9 values)
print(st2d.query(0, 2, 0, 2))   # 12 = 1 + 2 + 4 + 5
st2d.update(1, 1, 50)           # grid[1][1]: 5 -> 50 (delta +45)
print(st2d.query(0, 2, 0, 2))   # 57 = 1 + 2 + 4 + 50
// 2D Segment Tree -- point update, rectangle sum. O(log R * log C) per op.
#include <iostream>
#include <vector>
using namespace std;

class SegTree1D {
    int n;
    vector<long long> tree;
public:
    SegTree1D(const vector<long long>& values) {
        n = values.size();
        tree.assign(2 * n, 0);
        for (int i = 0; i < n; i++) tree[n + i] = values[i];
        for (int i = n - 1; i >= 1; i--) tree[i] = tree[2*i] + tree[2*i+1];
    }
    void add(int pos, long long delta) {
        pos += n;
        tree[pos] += delta;
        for (pos /= 2; pos >= 1; pos /= 2) tree[pos] = tree[2*pos] + tree[2*pos+1];
    }
    long long rangeSum(int l, int r) {  // half-open [l, r)
        long long res = 0;
        for (l += n, r += n; l < r; l /= 2, r /= 2) {
            if (l & 1) res += tree[l++];
            if (r & 1) res += tree[--r];
        }
        return res;
    }
};

class SegmentTree2D {
    int rows, cols;
    vector<vector<long long>> grid;
    vector<SegTree1D*> outer;
public:
    SegmentTree2D(const vector<vector<long long>>& g) {
        grid = g;
        rows = grid.size();
        cols = rows ? grid[0].size() : 0;
        outer.assign(2 * rows, nullptr);
        for (int r = 0; r < rows; r++) outer[rows + r] = new SegTree1D(grid[r]);
        for (int r = rows - 1; r >= 1; r--) {
            vector<long long> merged(cols);
            for (int c = 0; c < cols; c++)
                merged[c] = outer[2*r]->rangeSum(c, c+1) + outer[2*r+1]->rangeSum(c, c+1);
            outer[r] = new SegTree1D(merged);
        }
    }

    void update(int r, int c, long long val) {
        long long delta = val - grid[r][c];
        grid[r][c] = val;
        int pos = rows + r;
        outer[pos]->add(c, delta);
        for (pos /= 2; pos >= 1; pos /= 2) outer[pos]->add(c, delta);
    }

    long long query(int r1, int r2, int c1, int c2) {  // rows [r1,r2), cols [c1,c2)
        long long total = 0;
        for (int l = r1 + rows, r = r2 + rows; l < r; l /= 2, r /= 2) {
            if (l & 1) total += outer[l++]->rangeSum(c1, c2);
            if (r & 1) total += outer[--r]->rangeSum(c1, c2);
        }
        return total;
    }
};

int main() {
    vector<vector<long long>> grid = {{1,2,3},{4,5,6},{7,8,9}};
    SegmentTree2D st2d(grid);
    cout << st2d.query(0, 3, 0, 3) << endl;   // 45
    cout << st2d.query(0, 2, 0, 2) << endl;   // 12
    st2d.update(1, 1, 50);
    cout << st2d.query(0, 2, 0, 2) << endl;   // 57
}
// 2D Segment Tree -- point update, rectangle sum. O(log R * log C) per op.
public class SegmentTree2D {
    static class SegTree1D {
        int n;
        long[] tree;
        SegTree1D(long[] values) {
            n = values.length;
            tree = new long[2 * n];
            for (int i = 0; i < n; i++) tree[n + i] = values[i];
            for (int i = n - 1; i >= 1; i--) tree[i] = tree[2*i] + tree[2*i+1];
        }
        void add(int pos, long delta) {
            pos += n;
            tree[pos] += delta;
            for (pos /= 2; pos >= 1; pos /= 2) tree[pos] = tree[2*pos] + tree[2*pos+1];
        }
        long rangeSum(int l, int r) {  // half-open [l, r)
            long res = 0;
            for (l += n, r += n; l < r; l /= 2, r /= 2) {
                if ((l & 1) == 1) res += tree[l++];
                if ((r & 1) == 1) res += tree[--r];
            }
            return res;
        }
    }

    int rows, cols;
    long[][] grid;
    SegTree1D[] outer;

    SegmentTree2D(long[][] g) {
        rows = g.length;
        cols = rows > 0 ? g[0].length : 0;
        grid = new long[rows][];
        for (int r = 0; r < rows; r++) grid[r] = g[r].clone();
        outer = new SegTree1D[2 * rows];
        for (int r = 0; r < rows; r++) outer[rows + r] = new SegTree1D(grid[r]);
        for (int r = rows - 1; r >= 1; r--) {
            long[] merged = new long[cols];
            for (int c = 0; c < cols; c++)
                merged[c] = outer[2*r].rangeSum(c, c+1) + outer[2*r+1].rangeSum(c, c+1);
            outer[r] = new SegTree1D(merged);
        }
    }

    void update(int r, int c, long val) {
        long delta = val - grid[r][c];
        grid[r][c] = val;
        int pos = rows + r;
        outer[pos].add(c, delta);
        for (pos /= 2; pos >= 1; pos /= 2) outer[pos].add(c, delta);
    }

    long query(int r1, int r2, int c1, int c2) {  // rows [r1,r2), cols [c1,c2)
        long total = 0;
        for (int l = r1 + rows, r = r2 + rows; l < r; l /= 2, r /= 2) {
            if ((l & 1) == 1) total += outer[l++].rangeSum(c1, c2);
            if ((r & 1) == 1) total += outer[--r].rangeSum(c1, c2);
        }
        return total;
    }

    public static void main(String[] args) {
        long[][] grid = {{1,2,3},{4,5,6},{7,8,9}};
        SegmentTree2D st2d = new SegmentTree2D(grid);
        System.out.println(st2d.query(0, 3, 0, 3));  // 45
        System.out.println(st2d.query(0, 2, 0, 2));  // 12
        st2d.update(1, 1, 50);
        System.out.println(st2d.query(0, 2, 0, 2));  // 57
    }
}

For the $3 \times 3$ grid above, the outer root's inner tree holds column sums [12, 15, 18] (each column summed across all three rows), so query(0, 3, 0, 3) resolves at the root: $12+15+18=45$. After update(1, 1, 50) (row 1, column 1: $5 \to 50$, delta $+45$), that same delta is added to column 1 in the inner trees of every outer ancestor of row 1's leaf — the row-1 leaf itself, and the root — which is exactly why the second query(0, 2, 0, 2) reflects the change without a full rebuild.

9. Complexity Comparison

VariantTime per OperationSpaceBest For
Iterative (bottom-up)$O(\log n)$ update / query$O(n)$Point update + range query with minimal per-call overhead; tight loops in performance-critical code
Lazy Propagation$O(\log n)$ range update / range query$O(n)$Range updates combined with range queries (range-add range-sum, range-assign range-max)
Segment Tree Beats$O(\log^2 n)$ amortized update, $O(\log n)$ query$O(n)$Range chmin/chmax combined with range sum, where a plain lazy tag can't represent the update
Merge-Sort Tree$O(\log n)$ build per node, $O(\log^2 n)$ query$O(n \log n)$Offline rank/count-in-range queries with no updates
Dynamic / Implicit$O(\log U)$ update / query$O(Q \log U)$Point updates over huge or sparse coordinate ranges (up to $10^9$ or more)
2D Segment Tree$O(\log R \log C)$ update / query$O(R \cdot C)$2D range-sum or range-min with point updates over a grid

10. Applications & Alternatives

Where each variant shows up in practice

  • Iterative: competitive-programming judges with tight time limits, where the constant-factor savings from removing recursion decide whether a solution passes.
  • Lazy propagation: range-add leaderboards and bonus systems, interval scheduling with bulk range assignment, sweep-line algorithms in computational geometry.
  • Segment Tree Beats: range chmin/chmax problems (rate-limiting a batch of counters, capping resource pools) combined with aggregate queries — the canonical reference problem is HDU 5306.
  • Merge-Sort Tree: offline order-statistics queries, "$k$-th smallest in range" without updates, counting inversions restricted to a subrange.
  • Dynamic/Implicit: counting inversions or coordinate-compressed events over a huge value or timestamp domain where only a handful of positions are ever touched.
  • 2D: submatrix-sum queries over images or heatmaps, board-game state queries (count pieces in a rectangular region), spatial aggregation dashboards.

Alternatives worth knowing before reaching for a segment tree variant: two ordinary Fenwick (BIT) trees together can implement range-add/range-sum more simply than a lazy segment tree, at the cost of only supporting sum (not min/max/gcd). A sparse table answers static range min/max in $O(1)$ per query when there are no updates at all — cheaper than any variant here if the array never changes. A wavelet tree answers the same rank/count-in-range queries as the merge-sort tree in less space, at the cost of a trickier implementation. And when the workload needs to query every historical version of the array rather than just apply updates over huge coordinates, reach for the persistent segment tree instead of the dynamic/implicit tree described here — the two are often combined (a persistent tree over an implicit coordinate range) in the hardest competitive-programming problems.

11. Exercises

Easy

  1. Implement the iterative (bottom-up) segment tree for range-sum queries from scratch, using the recursive version in the Advanced Structures lesson as your reference for what the correct answers should be. Verify your iterative version returns the same results for at least five random range queries.
  2. Modify your iterative tree to support range-min instead of range-sum, and confirm it still passes the same five queries (adjusted for the new operation).

Medium

  1. Build a lazy-propagation segment tree over the array [10, 4, 8, 2, 6, 12, 1]. Apply update_range(1, 5, 3) (add 3 to indices 1 through 5), then query the sum of indices 0 through 6. By hand, trace which nodes apply the update directly versus which ones only see it after a forced push-down.
  2. Extend the same structure to support range assignment (set every element in $[l, r]$ to a fixed value) instead of range add, and explain why the lazy tag now needs a "has a pending assignment" flag in addition to a value.

Hard

  1. Using the Merge-Sort Tree, implement a function that counts how many elements in $[l, r]$ are $\le x$ for an array of at least 12 elements, and verify it against a brute-force linear scan for 10 random $(l, r, x)$ triples.
  2. Implement range chmax (for every index in $[l, r]$, set $arr[i] = \max(arr[i], x)$) combined with range-sum queries, as the mirror image of Segment Tree Beats' range chmin. You will need to track the minimum value, the second-minimum, and a count-of-minimum per node instead of the maximum-side statistics used in this lesson — every pruning condition flips accordingly.
Mastery challenge: Design a data structure for a game server that must, for a batch of up to $10^5$ players indexed by a 64-bit player ID (not a small dense range): (1) cap a range of players' scores at a maximum value ($O(\log^2 n)$-amortized chmin), (2) add a bonus to a range of players' scores, and (3) query the sum of scores in a range — while never allocating more memory than the number of players actually touched. Explain which two variants from this lesson you would combine, and where the combination breaks down if op (1) and op (2) are both required on the same structure without further modification.

Review rhythm: tomorrow, re-derive the lazy-propagation push-down order from memory (apply-then-clear, always before recursing past a node) without looking back at the code. Next week, re-implement Segment Tree Beats' three-way branch (skip / apply-in-O(1) / recurse) from the core insight alone. Several weeks later, work through the mastery challenge above and check whether your combined design matches a real "persistent + implicit" or "beats + lazy" structure you find in a competitive-programming reference.

12. Summary Cheat Sheet

  • Iterative segment tree: flat array of size $2n$, leaves at $[n, 2n)$, parent of $i$ is $\lfloor i/2 \rfloor$ — removes recursion overhead, but typically uses half-open query ranges.
  • Lazy propagation: apply a pending update to a node's own aggregate immediately, defer pushing it to children until forced to descend past that node — turns range updates from $O(n)$ into $O(\log n)$.
  • Segment Tree Beats: track max, second-max, and count-of-max per node to skip subtrees a chmin update provably can't affect, or fix them in $O(1)$ — $O(\log^2 n)$ amortized via a potential-function argument.
  • Merge-Sort Tree: each node stores a sorted copy of its subarray, enabling $O(\log^2 n)$ count-$\le x$-in-range queries via per-level binary search; no updates in the basic form.
  • Dynamic/implicit segment tree: allocate nodes lazily via left/right pointers instead of array indices, so a huge or sparse coordinate universe costs only $O(\log U)$ per touched position.
  • 2D segment tree: an outer segment tree of inner segment trees, giving $O(\log R \log C)$ rectangle queries at the cost of $O(R \cdot C)$ space.
  • Always know which of the two prerequisite structures — the plain recursive tree or the persistent tree — a new requirement actually calls for before reaching for a more exotic variant.