A lazy segment tree extended to handle updates that cannot be applied to a whole node in — most famously chmin (range “set every element to ”).

The problem

Ordinary lazy propagation requires a tag that transforms a node’s aggregate in . For chmin(l, r, x), knowing only a node’s sum and range length is not enough — the new sum depends on how many elements exceed and by how much.

The fix: store more, and recurse when in doubt

Each node keeps:

  • max1 — the maximum,
  • max2 — the strict second maximum ( if all equal),
  • cnt — how many elements equal max1,
  • sum.

To apply chmin(x) at a node:

CaseAction
nothing changes — return
only the maximums change: sum -= (max1 - x) * cnt; max1 = x;, tag it
ambiguous — recurse into both children, then pull up

The third case is the “beats” part: recursion is allowed, and the amortized analysis shows it does not happen often.

void applyMin(int node, long long x) {
    if (x >= mx1[node]) return;
    sum[node] -= (mx1[node] - x) * cntMax[node];
    mx1[node] = x;
}
void update(int node, int l, int r, int ql, int qr, long long x) {
    if (qr < l || r < ql || x >= mx1[node]) return;            // case 1: prune
    if (ql <= l && r <= qr && mx2[node] < x) { applyMin(node, x); return; }  // case 2
    push(node);                                                 // case 3: recurse
    int m = (l + r) / 2;
    update(2*node, l, m, ql, qr, x);
    update(2*node+1, m+1, r, ql, qr, x);
    pull(node);
}

The complexity argument

Define a potential . Initially .

  • A chmin that hits case 2 merges two distinct values into one, decreasing .
  • Each recursion in case 3 is paid for by a corresponding decrease.

Total: amortized for chmin plus range sum. With only chmin and range max (no sum), it improves to .

What it supports

UpdateQueryComplexity
chmin / chmaxsum, max, min am.
chmin + range addsum, max am.
chmin + chmax + addsum, max, min am.
range assignanything — ordinary lazy
”add to all divisible by sumpossible with more stored state
range sum, gcdworks — values only shrink, bounded by steps
range sumworks — values shrink fast
range sum✘ — no bound on the potential

The general principle: beats works when the update makes values converge. Operations that spread values apart have no potential argument and no bound.

Combining with range add

Adding chmin and range add requires two lazy tags: one for the maximum elements and one for the rest, since they were shifted differently. This is the standard “Ji Driver Segment Tree” and it is genuinely fiddly — write it once, test it against a brute force, and keep it in a template file.

Simpler alternatives to check first

SituationSimpler tool
Only range assign, no chminordinary lazy segment tree
Range assign + random-ish data”Chtholly tree” — an interval map in a std::map, expected
Only queries about the max, no sumordinary lazy segment tree with a max tag
smallsqrt decomposition with per-block sorted lists
Offlinesort by value and sweep

The Chtholly tree

For problems dominated by range assign, keep the array as a std::map<int, pair<int,int>> of maximal constant intervals. Assignment merges an entire range into one node; other operations iterate the (few) surviving intervals. On random data the number of intervals stays amortized, giving very fast solutions in about 40 lines. It is not worst-case safe, so check the constraints.

See also: Lazy Propagation · Segment Tree · Sqrt Decomposition