DSA DSA In Depth
0%
lesson lazy-propagationsegment-tree-beatsrange-updates

Lazy Propagation & Segment Tree Beats (Ji Driver Segment Tree for Range Chmin/Chmax)

7 min read Module 3.3: Range Query & Sublinear Decomposition Structures Updated Oct 23, 2026
📄 Cheat Sheet
Lazy Propagation Segment Tree — Range Updates in O(log N)
Tushar Roy - Coding Made Simple Lazy Propagation Segment Tree — Range Updates in O(log N) Watch on YouTube ↗

Shows how deferring O(N) descendant updates into O(log N) lazy tags preserves logarithmic time for both range updates and range queries.

0:00 / --:--

1. The Algebraic Axioms of Lazy Propagation (Monoid Action)

Formally, a Lazy Segment Tree requires two monoids and a distributive action:

  1. 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)$.
  2. 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)$.
  3. 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)$.
Segment Tree Beats Three-Case Tag Condition (Range Chmin $A[i] \leftarrow \min(A[i], x)$): At node $v$ maintaining $(\text{sum}, \text{max}_1, \text{cntMax}, \text{max}_2)$:
  1. Break Condition ($x \ge \text{max}_1$): No element in $v$'s subtree exceeds $x$; return immediately in $O(1)$ (no-op).
  2. 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)$!
  3. 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)$.
Segment Tree Beats: Range Chmin(l, r, x) Decision Cases at Node v Case 1: x >= v.max1 Break Condition (No-Op) Every element in v is already <= x. Action: return immediately Cost: O(1) Case 2: v.max2 < x < v.max1 Tag Condition (Lazy Clamp) Only the cntMax copies of max1 are clamped to x. sum -= (max1 - x) * cntMax max1 = x; Cost: O(1) Case 3: x <= v.max2 Recurse Condition Both max1 and max2 exceed x; they merge into a single value! pushDown(v); recurse L & R Amortized by potential drop
flowchart TD Start["chmin(v, tl, tr, ql, qr, x)"] --> Cond1{"ql > tr OR qr < tl OR x >= v.max1?"} Cond1 -- Yes --> Ret["Return O(1) (Break)"] Cond1 -- No --> Cond2{"ql <= tl AND tr <= qr AND x > v.max2?"} Cond2 -- Yes --> Apply["v.sum -= (v.max1 - x) * v.cntMax v.max1 = x Return O(1) (Tag)"] Cond2 -- No --> Push["pushDown(v) (clamp children to v.max1) Recurse Left & Right pull(v)"]
Hardware & Cache Reality — Struct-of-Arrays (SoA) TypedArrays for Lazy Segment Trees: A Segment Tree Beats node tracks 4 fields (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.
Segment Tree Monoids, Lazy Propagation & Ji Driver Beats
O(log N) Lazy · O(log² N) Beats

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.

Step 1 / 9
Custom Input:
Timeline

Invariant & State

Initial Segment Tree over A = [1, 2, 3, 4, 5, 6, 7, 8]. All lazy[v] = 0.
🌳 Live 2D Hierarchical Tree Topology — Segment Tree Monoids, Lazy Propagation & Ji Driver Beats Visited/Active Pivot/Root Resolved
Root [0..7… [0] Left [0..3… [1] Right [4..7… [2] [0..1… [3] [2..3… [4] [4..5… [5] [6..7… [6]
[0][0..7]:36Root
[1][0..3]:10Left
[2][4..7]:26Right
[3][0..1]:3
[4][2..3]:7
[5][4..5]:11
[6][6..7]:15
Canonical Nodes Hit
≤ 2 per Tree Level
Total Nodes Visited
O(log N)
Lazy Tag Rule
tree[v] += len(v) · δ
[Step 1] Built segment tree over [0..7]. Executing rangeAdd(L=2, R=5, delta=+10).

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
Common Pitfall — Confusing Strict Second Maximum (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!
Check yourself Question 1 of 4
Concept Check Distributivity Requirement in Lazy Propagation recall

In a Lazy Segment Tree with data monoid $(S, \oplus)$ and lazy tag monoid $(F, \circ)$, why must every lazy tag $f \in F$ satisfy $f(s_L \oplus s_R) = f(s_L) \oplus f(s_R)$ (or depend only on the known segment length)?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why does this seemingly reasonable `_pull(v)` implementation cause TLE ($\Theta(N)$ per query) when the array contains equal elements?

Key Takeaways

Lazy propagation requires a Data Monoid (S, op) and an Endomorphism Tag Monoid (F, compose) whose action distributes over op: f(s1 op s2) = f(s1) op f(s2).
When combining two non-commutative lazy operations like range assignment/scaling and range addition, compose them as an affine transformation f(x) = a*x + b where f2(f1(x)) = (a2*a1)*x + (a2*b1 + b2).
Segment Tree Beats handles non-linear range updates (chmin/chmax with range sum) by tracking max1, cntMax, and strict second maximum max2 at every node.
Whenever x <= max2, recursing into children merges max1 and max2 into a single value x, reducing the tree's potential function and guaranteeing O(log N) (or O(log^2 N)) amortized time.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue