Range updates in : when an update covers a whole node, apply it to the node’s summary and leave a tag for the children, pushing it down only when a later operation enters the node. This works iff:

  1. a tag can update a node’s summary in from the summary alone (plus things like the length, which you store in the node);
  2. tags compose: two pending updates merge into one tag.

When rule 1 fails, look at segment tree beats or sqrt decomposition.

Generic lazy segment tree

// S: node summary, F: tag. op(S,S), e(), mapping(F,S) -> S, composition(F new, F old) -> F, id()
template<class S, S (*op)(S, S), S (*e)(), class F, S (*mapping)(F, S), F (*composition)(F, F), F (*id)()>
struct LazySegTree {
    int n, size, lg; vector<S> d; vector<F> lz;
    LazySegTree(const vector<S>& a) : n(sz(a)) {
        lg = 0; while ((1 << lg) < n) lg++; size = 1 << lg;
        d.assign(2 * size, e()); lz.assign(size, id());
        copy(all(a), d.begin() + size);
        for (int i = size - 1; i > 0; i--) pull(i);
    }
    void pull(int k) { d[k] = op(d[2 * k], d[2 * k + 1]); }
    void apply1(int k, F f) { d[k] = mapping(f, d[k]); if (k < size) lz[k] = composition(f, lz[k]); }
    void push(int k) { apply1(2 * k, lz[k]); apply1(2 * k + 1, lz[k]); lz[k] = id(); }
    S prod(int l, int r) {                               // [l, r)
        if (l == r) return e();
        l += size; r += size;
        for (int i = lg; i >= 1; i--) {
            if (((l >> i) << i) != l) push(l >> i);
            if (((r >> i) << i) != r) push((r - 1) >> i);
        }
        S sl = e(), sr = e();
        for (; l < r; l >>= 1, r >>= 1) { if (l & 1) sl = op(sl, d[l++]); if (r & 1) sr = op(d[--r], sr); }
        return op(sl, sr);
    }
    void apply(int l, int r, F f) {                      // [l, r)
        if (l == r) return;
        l += size; r += size;
        for (int i = lg; i >= 1; i--) {
            if (((l >> i) << i) != l) push(l >> i);
            if (((r >> i) << i) != r) push((r - 1) >> i);
        }
        for (int a = l, b = r; a < b; a >>= 1, b >>= 1) { if (a & 1) apply1(a++, f); if (b & 1) apply1(--b, f); }
        for (int i = 1; i <= lg; i++) {
            if (((l >> i) << i) != l) pull(l >> i);
            if (((r >> i) << i) != r) pull((r - 1) >> i);
        }
    }
};

This is the AtCoder Library design: iterative, about 2x faster than recursive, and every problem is “define S, F, and the five functions”. Descents (maxRight, minLeft) extend the same way as in the plain tree (push on the way down).

Common tag algebras

UpdateQuerySF and mapping
addsum(sum, len): sum len
addmin/maxvalue: value
assignsum(sum, len)optional value: sum len
affine sum mod (sum, len): sum sum len; compose
assign + addanythingencode assign as and add as in the affine algebra
flip bits (0/1)count ones(ones, len)flip: ones len ones
add arithmetic progression sum(sum, len); the right child receives ; tags add up
range XOR with , query sumper-bit counts (ones per bit, len): flip counts of bits set in
assign, query “number of distinct runs”(runs, left value, right value)assign: runs = 1
range assign, query max subarray sum(sum, pre, suf, best, len)assign : everything becomes a function of and len
range add, query max subarray sumnot expressible as a lazy tag: use sqrt decomposition with per-block convex hulls (KTT, kinetic segment tree)
range sort ascending/descendingnot lazy: sortable segment tree (Merging)

Order of composition: composition(new, old) means “apply old first, then new”. Get it backwards and simple tests pass while interleaved updates fail. Test against a brute force with random interleaving.

Tag permanence (no push-down)

For commutative tags (adds), keep each tag on the node forever and add its effect while descending during queries. Queries never modify the tree, which is exactly what persistent and dynamic trees need (no node copies on query), and what makes 2D “segment tree of segment trees” range add possible for sums.

// add v on [x, y) inside node p covering [l, r): sum[p] += v * overlap; if fully covered: tag[p] += v; else recurse
// query: if fully covered return sum[p]; else tag[p] * overlap + children

Recurring problems

  • Range add, range min with “count of minimum”: count zeros after adds (cover length of rectangles in sweeps: min means uncovered).
  • Range assign, range sum with coordinates: dynamic lazy tree or compress coordinates.
  • “Paint” problems: assign with colour, query count of a colour in range: per-node “uniform colour or mixed”, recurse into mixed nodes; amortized like Chtholly.
  • Range , , : not lazy; recurse while the node’s max shows the update matters. Each element changes times (mod: when it changes, it at least halves), so the total is amortized (Beats).
  • Range add, range gcd: store the gcd of the difference array in a plain segment tree plus a Fenwick for values: , and a range add changes only two differences.

Alternatives that are shorter

SituationTool
range add, point queryFenwick on the difference array
range add, range sumtwo Fenwicks
all updates before all queriesdifference array
range assign only, random dataChtholly tree