Some updates can’t be applied to a whole node in : chmin needs to know how many elements exceed , and mod x changes every element differently. Segment tree beats (Ji Ruyi, 2016) stores just enough to handle the easy cases in and recurses in the hard ones, with a potential argument bounding the total recursion. Historic-value trees track “the maximum each element ever had” or “the sum over time” with tags that remember the best prefix of the update sequence.
Chmin, chmax, add with sum, max and min
Each node stores max, strict second max, count of max (and the same for min) plus the sum. chmin(x) on a node:
- : nothing to do;
- : only the maximum elements change:
sum -= (max1 - x) * cnt,max1 = x, tag it; - : recurse into the children.
struct Beats {
static constexpr ll INF = LLONG_MAX / 4;
struct N { ll mx1, mx2, mn1, mn2, cmx, cmn, sum, add; int len; };
vector<N> t; int n;
Beats(const vector<ll>& a) : n(sz(a)), t(4 * sz(a)) { build(1, 0, n, a); }
void pull(int v) {
N &x = t[v], &L = t[2 * v], &R = t[2 * v + 1];
x.sum = L.sum + R.sum;
if (L.mx1 == R.mx1) x.mx1 = L.mx1, x.cmx = L.cmx + R.cmx, x.mx2 = max(L.mx2, R.mx2);
else if (L.mx1 > R.mx1) x.mx1 = L.mx1, x.cmx = L.cmx, x.mx2 = max(L.mx2, R.mx1);
else x.mx1 = R.mx1, x.cmx = R.cmx, x.mx2 = max(L.mx1, R.mx2);
if (L.mn1 == R.mn1) x.mn1 = L.mn1, x.cmn = L.cmn + R.cmn, x.mn2 = min(L.mn2, R.mn2);
else if (L.mn1 < R.mn1) x.mn1 = L.mn1, x.cmn = L.cmn, x.mn2 = min(L.mn2, R.mn1);
else x.mn1 = R.mn1, x.cmn = R.cmn, x.mn2 = min(L.mn1, R.mn2);
}
void build(int v, int l, int r, const vector<ll>& a) {
t[v].len = r - l; t[v].add = 0;
if (r - l == 1) { t[v] = {a[l], -INF, a[l], INF, 1, 1, a[l], 0, 1}; return; }
int m = (l + r) / 2; build(2 * v, l, m, a); build(2 * v + 1, m, r, a); pull(v);
}
void applyAdd(int v, ll x) {
N& a = t[v]; a.sum += x * a.len; a.mx1 += x; a.mn1 += x; a.add += x;
if (a.mx2 != -INF) a.mx2 += x;
if (a.mn2 != INF) a.mn2 += x;
}
void applyMin(int v, ll x) { // requires mx2 < x < mx1
N& a = t[v]; if (a.mx1 <= x) return;
a.sum -= (a.mx1 - x) * a.cmx;
if (a.mn1 == a.mx1) a.mn1 = x; // node with one or two distinct values
else if (a.mn2 == a.mx1) a.mn2 = x;
a.mx1 = x;
}
void applyMax(int v, ll x) { // requires mn1 < x < mn2
N& a = t[v]; if (a.mn1 >= x) return;
a.sum += (x - a.mn1) * a.cmn;
if (a.mx1 == a.mn1) a.mx1 = x;
else if (a.mx2 == a.mn1) a.mx2 = x;
a.mn1 = x;
}
void push(int v) {
for (int c : {2 * v, 2 * v + 1}) {
if (t[v].add) applyAdd(c, t[v].add);
applyMin(c, t[v].mx1); applyMax(c, t[v].mn1); // children never exceed the parent's extremes
}
t[v].add = 0;
}
void chmin(int v, int l, int r, int ql, int qr, ll x) {
if (qr <= l || r <= ql || t[v].mx1 <= x) return;
if (ql <= l && r <= qr && t[v].mx2 < x) { applyMin(v, x); return; }
push(v); int m = (l + r) / 2;
chmin(2 * v, l, m, ql, qr, x); chmin(2 * v + 1, m, r, ql, qr, x); pull(v);
}
void chmax(int v, int l, int r, int ql, int qr, ll x) {
if (qr <= l || r <= ql || t[v].mn1 >= x) return;
if (ql <= l && r <= qr && t[v].mn2 > x) { applyMax(v, x); return; }
push(v); int m = (l + r) / 2;
chmax(2 * v, l, m, ql, qr, x); chmax(2 * v + 1, m, r, ql, qr, x); pull(v);
}
void add(int v, int l, int r, int ql, int qr, ll x) {
if (qr <= l || r <= ql) return;
if (ql <= l && r <= qr) { applyAdd(v, x); return; }
push(v); int m = (l + r) / 2;
add(2 * v, l, m, ql, qr, x); add(2 * v + 1, m, r, ql, qr, x); pull(v);
}
ll sum(int v, int l, int r, int ql, int qr) {
if (qr <= l || r <= ql) return 0;
if (ql <= l && r <= qr) return t[v].sum;
push(v); int m = (l + r) / 2;
return sum(2 * v, l, m, ql, qr) + sum(2 * v + 1, m, r, ql, qr);
}
};Complexity: chmin/chmax with sum queries is amortized without adds. With range adds it’s . Potential: the number of distinct “max tags” across nodes; a recursion into a child happens only when two distinct values merge.
Proof: the three-case update is correct, including the min/max-tracking patch
The three cases partition every query by comparing to (recall always), and are exhaustive. Case 1 () is correctly a no-op since for every . Case 3 () makes no claim, only recursing, so it cannot be wrong. Case 2 (): every element is already and untouched; the only elements that change are the elements equal to , each dropping by exactly , so
sum -= (mx1-x)*cmxis exact. The new is , and is left alone correctly whenever the node has distinct values (the old , being the strict second-largest, still sits strictly below the new since ). The min side needs patching only when the node has distinct values, which is exactly when coincides with something the min-structure is tracking: if the whole range is one value, and that value (hence the range’s minimum too) is now ; if the range has exactly two distinct values , and the min-side’s second-distinct value was , now . With distinct values, (the second-smallest of values) cannot equal (the largest), so neither branch fires and none is needed — the code’s two checks cover exactly these two patch cases and no others.
Proof: amortized without range add
For every node and at any instant, let be the number of distinct array values currently occurring among the leaves in ‘s range, and let over all nodes.
Initial bound. , and ( per level, levels), so .
Monotone. chmin/chmax never increase any : they only map some values down to a common value , merging distinct classes, never splitting one value into two.
Case 2 doesn’t change at that node. A direct apply renames the -class to ; since , every other value stays strictly below the new maximum, so ‘s own distinct-value count is unchanged by its own application (nothing merges at ).
Every case-3 visit pays for itself. Let be any node visited in case 3 during some
chmin(x)call (, so this is true regardless of whether started the cascade or is buried deep inside one). Let be the set of distinct values in ‘s range that are before this call reaches ; , so . By the time the recursive call on ‘s subtree finishes, every element whose value was in equals exactly (that is whatchmin(\cdot,x)does to every value , wherever it is finally resolved), and every element is untouched. So the distinct values collapse into the single value , and drops by exactly . Charge the overhead of visiting in case 3 (the dispatch,push,pull, and launching the two child calls) to this drop.Summing the charges. only decreases over the algorithm’s run and always stays , so can be charged (visited in case 3) at most times in total, ever — consistent with the data structure itself: once ,
pullforces (by induction, since both children then also have and ), so case 3 (which needs a finite ) can never fire at again. Summed over all nodes, the total number of case-3 visits across the entire run is at most .Total cost. Each of the operations additionally does the ordinary range-decomposition work any segment tree does (independent of beats), and work per case-1/case-2 terminal; since the case-3 nodes visited during one call form a binary tree whose terminals are case-1/case-2 nodes, the terminal count is (case-3 count) per cascade, so terminals are charged for free alongside the case-3 total. Grand total: (decomposition) (case-3 visits, amortized over the whole run) .
With range add interleaved,
applyMin/applyMaxon a child duringpushcan shift relative to the untouched part of the range in a way that breaks the clean ” strictly drops” argument per visit (a pushed-down add can make a previously-equal value diverge again before the next chmin reaches it), costing an extra factor; this interaction is Ji Ruyi’s original result and is cited here rather than re-derived.
Test against a brute force with random interleavings; the two-or-fewer-distinct-values edge cases (mn1 == mx1, mn2 == mx1) are where bugs hide.
Other “recurse while it matters” updates
| Update | Stop condition | Bound |
|---|---|---|
| node max | each change at least halves : changes per element | |
| node max | changes per element | |
| with range add | node max min small: convert to add when | per “distance” potential |
| divisible by the node’s lcm (capped) | each change at least halves | |
| / | every element agrees on the affected bits: then it’s an add | per bit flip |
Total cost is , plus sums of updates when combined with adds (count the potential carefully).
Proof: the per-element change bounds
Mod. If (otherwise
mod xis a no-op), then ; moreover always: if this holds since ; if then , so . Either way at least halves, so it reaches after changes.
Sqrt. Repeated roughly halves each step (since up to rounding), so reaches after changes.
Gcd. If (otherwise the update is a no-op), is a proper divisor of , and the largest proper divisor of any integer is at most (its smallest prime factor is ), so at least halves per actual change, giving changes.
AND/OR. Track, per node, the OR (for AND-updates) or AND (for OR-updates) of its range, used exactly as is used above: the stop condition is “this node already agrees with on every bit would change,” and the ambiguous/recurse condition is the negation. The same potential argument as the main beats proof applies with in place of : , and AND/OR never increases any bit’s count (AND only clears bits, OR only sets them, monotonically), so by the identical charging argument the total number of forced recursions is , i.e. amortized changes per element.
Historic maximum
Each element also tracks over time of . With range add only, a node’s tag is a pair (total add, best prefix of the adds since the last push). Applying a tag to a node: hmx = max(hmx, mx + h), mx += a, and the node’s own tag updates hadd = max(hadd, add + h), add += a.
With range assign too, a node that has been assigned since the last push only shifts its assigned value: tags become (add part, best add prefix) before the first assignment and (assigned value, best assigned value) after it. Push applies the add part first, then the assignment part. That’s Luogu P4314 (“CPU monitoring”).
Proof: the historic-maximum tag composition is correct
A pending tag on a node means: every leaf under it has had applied to its current value since the last push, and at some point during that stretch (not necessarily now) its running value peaked at (current-value-before-this-stretch) for the best prefix of the adds applied so far, with (the peak is at least as large as the final total, since is itself one valid prefix). Applying to a child with its own current max and historic max : the child’s value peaked, during the parent’s stretch, at (its value before the stretch, , plus the best prefix of the adds applied since) — so correctly folds in this new peak without disturbing any higher peak from before the stretch. Then applies the actual net change. Composing the child’s own pending tag with the incoming : the combined stretch’s best prefix is the better of (child’s own best prefix , reached before any of the parent’s stretch happened) or (child’s full stretch followed by the parent’s best prefix , i.e. ) — so , matching
hadd = max(hadd, add + h), and matchesadd += a. Induction on the number of composed tags extends this to any pushed-down chain.
Historic sum (sum over time of , queried over ranges): keep the sum of and of the historic sums, plus a “time” tag: each tick adds the current sum to the historic sum. Lazy tags are affine in (value, history) and compose like matrices . This is the engine of offline “number of subarrays within with property” problems: sweep the right end, maintain for each left end a 0/1 or value, and accumulate history to count over all right ends.
Historic minimum, with chmin/chmax: combine beats with “best prefix” tags per class (max elements and others).