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 equalmax1,sum.
To apply chmin(x) at a node:
| Case | Action |
|---|---|
| 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
chminthat 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
| Update | Query | Complexity |
|---|---|---|
chmin / chmax | sum, max, min | am. |
chmin + range add | sum, max | am. |
chmin + chmax + add | sum, max, min | am. |
| range assign | anything | — ordinary lazy |
| ”add to all divisible by “ | sum | possible with more stored state |
| range | sum, gcd | works — values only shrink, bounded by steps |
| range | sum | works — 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
| Situation | Simpler tool |
|---|---|
| Only range assign, no chmin | ordinary lazy segment tree |
| Range assign + random-ish data | ”Chtholly tree” — an interval map in a std::map, expected |
| Only queries about the max, no sum | ordinary lazy segment tree with a max tag |
| small | sqrt decomposition with per-block sorted lists |
| Offline | sort 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