1. The Algebraic Axioms of Lazy Propagation (Monoid Action)
Formally, a Lazy Segment Tree requires two monoids and a distributive action:
- Data Monoid $(S, \oplus, e_S)$: Combines left and right child summaries (
pull/pushUp), e.g., $(S_L + S_R, \text{len}_L + \text{len}_R)$. - Tag (Endomorphism) Monoid $(F, \circ, \text{id}_F)$: Composes a new lazy operation $f_{\text{new}}$ onto an existing pending tag $f_{\text{old}}$ via $f_{\text{new}} \circ f_{\text{old}}$. For range affine updates $f(x) = ax + b$, composing $f_2(x) = a_2 x + b_2$ after $f_1(x) = a_1 x + b_1$ yields $f_2(f_1(x)) = (a_2 a_1)x + (a_2 b_1 + b_2)$.
- Distributive Mapping $F \times S \to S$: For any tag $f \in F$ and states $s_1, s_2 \in S$, applying $f$ distributes over $\oplus$: $f(s_1 \oplus s_2) = f(s_1) \oplus f(s_2)$.
- Break Condition ($x \ge \text{max}_1$): No element in $v$'s subtree exceeds $x$; return immediately in $O(1)$ (no-op).
- Tag Condition ($\text{max}_2 < x < \text{max}_1$): Exactly $\text{cntMax}$ elements in $v$'s subtree exceed $x$, and all of them equal $\text{max}_1$. Update $\text{sum} \mathrel{-}= (\text{max}_1 - x) \cdot \text{cntMax}$, set $\text{max}_1 = x$, and stop in $O(1)$!
- Recurse Condition ($x \le \text{max}_2$): At least two distinct values in $v$'s subtree collapse into $x$; push down tags, recurse to both children, and
pull(v). Each recursive split permanently reduces the number of distinct value classes in $v$'s subtree by at least 1, bounding total amortized work to $O((N + Q)\log N)$.
sum, max1, max2, cntMax). Storing 4 separate flat TypedArrays (Float64Array for sum and Int32Array for max1, max2, cntMax) of length $4N$ eliminates pointer chasing and object headers completely, packing 16 Int32 values into a single 64-byte cache line.
Watch how a range update [L, R] halts at maximal canonical nodes with a deferred lazy[v] tag, or switch to Segment Tree Beats to see how tracking (max1, smax, cntMax) turns range chmin(x) into a lazy tag when smax < x < max1.
Invariant & State
2. Production Implementation: Segment Tree Beats (Range Chmin + Range Sum)
/**
* Ji Driver Segment Tree (Segment Tree Beats)
* Supports:
* - rangeChmin(ql, qr, x): for i in [ql..qr], A[i] = Math.min(A[i], x) in O(log N) amortized
* - rangeSum(ql, qr): sum of A[ql..qr] in O(log N)
* - rangeMax(ql, qr): max of A[ql..qr] in O(log N)
*/
class SegmentTreeBeats {
constructor(arr) {
this.n = arr.length;
const size = 4 * this.n;
this.sum = new Float64Array(size);
this.max1 = new Float64Array(size);
this.max2 = new Float64Array(size);
this.cntMax = new Int32Array(size);
this._build(1, 0, this.n - 1, arr);
}
_build(v, tl, tr, arr) {
if (tl === tr) {
this.sum[v] = arr[tl];
this.max1[v] = arr[tl];
this.max2[v] = -Infinity;
this.cntMax[v] = 1;
return;
}
const tm = (tl + tr) >> 1;
this._build(v << 1, tl, tm, arr);
this._build((v << 1) | 1, tm + 1, tr, arr);
this._pull(v);
}
_pull(v) {
const L = v << 1, R = L | 1;
this.sum[v] = this.sum[L] + this.sum[R];
if (this.max1[L] === this.max1[R]) {
this.max1[v] = this.max1[L];
this.max2[v] = Math.max(this.max2[L], this.max2[R]);
this.cntMax[v] = this.cntMax[L] + this.cntMax[R];
} else if (this.max1[L] > this.max1[R]) {
this.max1[v] = this.max1[L];
this.max2[v] = Math.max(this.max2[L], this.max1[R]);
this.cntMax[v] = this.cntMax[L];
} else {
this.max1[v] = this.max1[R];
this.max2[v] = Math.max(this.max1[L], this.max2[R]);
this.cntMax[v] = this.cntMax[R];
}
}
_applyChmin(v, x) {
if (x >= this.max1[v]) return;
this.sum[v] -= (this.max1[v] - x) * this.cntMax[v];
this.max1[v] = x;
}
_pushDown(v) {
// Parent's max1 acts as an implicit upper-bound clamp on both children!
this._applyChmin(v << 1, this.max1[v]);
this._applyChmin((v << 1) | 1, this.max1[v]);
}
rangeChmin(ql, qr, x, v = 1, tl = 0, tr = this.n - 1) {
if (ql > tr || qr < tl || x >= this.max1[v]) return; // Case 1: Break
if (ql <= tl && tr <= qr && x > this.max2[v]) { // Case 2: Tag
this._applyChmin(v, x);
return;
}
this._pushDown(v); // Case 3: Recurse
const tm = (tl + tr) >> 1;
this.rangeChmin(ql, qr, x, v << 1, tl, tm);
this.rangeChmin(ql, qr, x, (v << 1) | 1, tm + 1, tr);
this._pull(v);
}
rangeSum(ql, qr, v = 1, tl = 0, tr = this.n - 1) {
if (ql > tr || qr < tl) return 0;
if (ql <= tl && tr <= qr) return this.sum[v];
this._pushDown(v);
const tm = (tl + tr) >> 1;
return (
this.rangeSum(ql, qr, v << 1, tl, tm) +
this.rangeSum(ql, qr, (v << 1) | 1, tm + 1, tr)
);
}
}
3. Complexity Matrix
| Structure & Operation Suite | Build Time | Range Update Time | Range Query Time |
|---|---|---|---|
| Standard Lazy Segment Tree (Range Affine $ax + b$, Range Sum) | $\Theta(N)$ | $O(\log N)$ worst-case | $O(\log N)$ worst-case |
Segment Tree Beats (Range chmin/chmax + Range Sum) |
$\Theta(N)$ | $O(\log N)$ amortized | $O(\log N)$ worst-case |
Segment Tree Beats (Range chmin/chmax + Range add + Range Sum) |
$\Theta(N)$ | $O(\log^2 N)$ amortized | $O(\log N)$ worst-case |
max2) with Non-Strict Second Element: In Segment Tree Beats, max2[v] MUST be the strictly second-largest distinct value in node $v$'s subtree (if all elements in $v$ are equal to $7$, then max1[v] = 7, cntMax[v] = tr - tl + 1, and max2[v] = -Infinity). If you accidentally set max2[v] = 7 when both children have max1 = 7, then x > max2[v] will never trigger for $x < 7$, causing your Segment Tree Beats to recurse all the way to the leaves on every update and degrade to $\Theta(N)$ per query!