DSA DSA In Depth
0%
build build-labsegment-treelazy-propagation

Build Lab: Generic Lazy-Propagating Segment Tree with Range Affine Updates & Queries

7 min read Module 3.3: Range Query & Sublinear Decomposition Structures Updated Oct 23, 2026
📄 Cheat Sheet
AlgorithmsThread 3: Segment Trees & Lazy Propagation
SecondThread AlgorithmsThread 3: Segment Trees & Lazy Propagation Watch on YouTube ↗

Walks through composing affine lazy tags f(x) = a*x + b so range assignment, range scaling, and range addition unify into a single bug-free lazy segment tree.

0:00 / --:--

1. Algebraic Specification of the Range Affine Lazy Segment Tree

Let each node $v$ covering $[tl, tr]$ of length $\text{len} = tr - tl + 1$ store:

  • Data Summary: $\text{sum}[v] = \sum_{i=tl}^{tr} A[i]$.
  • Lazy Affine Tag: $(\text{lazyA}[v], \text{lazyB}[v])$, initialized to the identity map $(1, 0)$.
Affine Tag Application & Composition Invariants: When applying affine tag $(a, b)$ to node $v$ of length $\text{len} = tr - tl + 1$:
  1. Summary Update: $\sum_{i=tl}^{tr} (a \cdot A[i] + b) = a \cdot \text{sum}[v] + b \cdot \text{len}$.
  2. Tag Composition ($f_{\text{new}} \circ f_{\text{old}}$): If $v$ already holds pending tag $(a_1, b_1)$, applying $(a_2, b_2)$ updates the tag to: $$\text{lazyA}[v] \leftarrow a_2 \cdot a_1, \qquad \text{lazyB}[v] \leftarrow a_2 \cdot b_1 + b_2$$ Notice that when $a_2 = 0$ (rangeAssign(l, r, c)), this automatically zeroes out $a_1$ and $b_1$, leaving $(0, c)$ with zero if/else branching!
Unified Range Affine Endomorphism: f(x) = a*x + b Covers Add, Scale, and Assign 1. Three Operations -> (a, b) rangeAdd(d) -> (1, d) rangeScale(k) -> (k, 0) rangeAssign(c) -> (0, c) Identity Tag -> (1, 0) 2. Apply (a, b) to Node v sum[v] = a * sum[v] + b * len lazyA[v] = a * lazyA[v] lazyB[v] = a * lazyB[v] + b Zero conditional branches! 3. Tree Walk maxRight(l, pred) Finds largest r >= l where pred(sum(l..r)) is true in Single-Pass O(log N) (vs O(log^2 N) binary search)
flowchart TD MR["maxRight(v, tl, tr, l, pred, acc)"] --> Out{"tr < l?"} Out -- Yes --> RetTr["Return tr (before l)"] Out -- No --> Inside{"tl >= l AND pred(combine(acc, sum[v]))?"} Inside -- Yes --> Consume["acc = combine(acc, sum[v]) Return tr (consume whole subtree O(1)!)"] Inside -- No --> Leaf{"tl == tr?"} Leaf -- Yes --> Stop["Return tl - 1 (boundary reached)"] Leaf -- No --> Push["pushDown(v); recurse Left; if Left fully consumed, recurse Right"]
Hardware & Cache Reality — Why Tree-Walk maxRight Beats Binary Search + Query by $O(\log N)$: Calling binary search over rangeSum(l, mid) performs $\log_2 N$ segment tree queries, each taking $O(\log N)$ time ($O(\log^2 N)$ total, ~400 node visits for $N = 10^5$). By descending directly inside the Segment Tree while maintaining a running accumulator acc, maxRight(l, pred) consumes at most $O(\log N)$ whole canonical subtrees in $O(1)$ each and descends down at most one leaf path where pred(combine(acc, sum[v])) first turns false—visiting at most $4 \log_2 N \approx 68$ nodes total!
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. Complete Production Implementation: Range Affine Lazy Segment Tree + Verification Suite

/**
 * Production Range Affine Lazy Segment Tree (Struct-of-Arrays TypedArray Layout)
 * Supports:
 *  - rangeAffine(ql, qr, a, b): for i in [ql..qr], A[i] = a * A[i] + b in O(log N)
 *  - rangeAdd(ql, qr, delta):   special case rangeAffine(ql, qr, 1, delta)
 *  - rangeScale(ql, qr, k):     special case rangeAffine(ql, qr, k, 0)
 *  - rangeAssign(ql, qr, val):  special case rangeAffine(ql, qr, 0, val)
 *  - rangeSum(ql, qr):          sum of A[ql..qr] in O(log N)
 *  - maxRight(l, pred):         largest r >= l-1 such that pred(sum(l..r)) is true in O(log N)
 */
class RangeAffineLazySegTree {
  constructor(arr) {
    this.n = arr.length;
    const size = 4 * this.n;
    this.sum = new Float64Array(size);
    this.lazyA = new Float64Array(size).fill(1);
    this.lazyB = new Float64Array(size).fill(0);
    if (this.n > 0) this._build(1, 0, this.n - 1, arr);
  }

  _build(v, tl, tr, arr) {
    if (tl === tr) {
      this.sum[v] = arr[tl];
      return;
    }
    const tm = (tl + tr) >> 1;
    this._build(v << 1, tl, tm, arr);
    this._build((v << 1) | 1, tm + 1, tr, arr);
    this.sum[v] = this.sum[v << 1] + this.sum[(v << 1) | 1];
  }

  _apply(v, len, a, b) {
    this.sum[v] = a * this.sum[v] + b * len;
    this.lazyA[v] = a * this.lazyA[v];
    this.lazyB[v] = a * this.lazyB[v] + b;
  }

  _pushDown(v, tl, tr) {
    const a = this.lazyA[v], b = this.lazyB[v];
    if (a === 1 && b === 0) return;
    const tm = (tl + tr) >> 1;
    this._apply(v << 1, tm - tl + 1, a, b);
    this._apply((v << 1) | 1, tr - tm, a, b);
    this.lazyA[v] = 1;
    this.lazyB[v] = 0;
  }

  rangeAffine(ql, qr, a, b, v = 1, tl = 0, tr = this.n - 1) {
    if (ql > tr || qr < tl) return;
    if (ql <= tl && tr <= qr) {
      this._apply(v, tr - tl + 1, a, b);
      return;
    }
    this._pushDown(v, tl, tr);
    const tm = (tl + tr) >> 1;
    this.rangeAffine(ql, qr, a, b, v << 1, tl, tm);
    this.rangeAffine(ql, qr, a, b, (v << 1) | 1, tm + 1, tr);
    this.sum[v] = this.sum[v << 1] + this.sum[(v << 1) | 1];
  }

  rangeAdd(ql, qr, delta) {
    this.rangeAffine(ql, qr, 1, delta);
  }

  rangeScale(ql, qr, factor) {
    this.rangeAffine(ql, qr, factor, 0);
  }

  rangeAssign(ql, qr, val) {
    this.rangeAffine(ql, qr, 0, val);
  }

  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, tl, tr);
    const tm = (tl + tr) >> 1;
    return (
      this.rangeSum(ql, qr, v << 1, tl, tm) +
      this.rangeSum(ql, qr, (v << 1) | 1, tm + 1, tr)
    );
  }

  /**
   * Single-pass O(log N) Segment Tree Walk (requires non-negative elements for monotone sum):
   * Returns largest index r in [l-1 .. n-1] such that pred(sum(l..r)) === true.
   */
  maxRight(l, pred) {
    let acc = 0;
    const walk = (v, tl, tr) => {
      if (tr < l) return tr;
      if (tl >= l && pred(acc + this.sum[v])) {
        acc += this.sum[v];
        return tr;
      }
      if (tl === tr) return tl - 1;
      this._pushDown(v, tl, tr);
      const tm = (tl + tr) >> 1;
      const resL = walk(v << 1, tl, tm);
      if (resL < tm) return resL; // Stopped inside left child!
      return walk((v << 1) | 1, tm + 1, tr);
    };
    return walk(1, 0, this.n - 1);
  }
}

3. Complexity Matrix

Method Time Complexity Nodes Visited (Worst Case) Extra Heap Allocations
constructor(arr) $\Theta(N)$ $2N - 1$ $3 \times 4N$ Float64Array
rangeAffine / rangeAdd / rangeAssign $O(\log N)$ $\le 4 \lceil \log_2 N \rceil$ $0$
rangeSum(ql, qr) $O(\log N)$ $\le 4 \lceil \log_2 N \rceil$ $0$
maxRight(l, pred) $O(\log N)$ $\le 4 \lceil \log_2 N \rceil$ $0$
Common Pitfall — Forgetting to Multiply $b$ by Segment Length len = tr - tl + 1: When applying an affine tag $(a, b)$ to a single element $x$, the new value is $ax + b$. When applying $(a, b)$ to a segment sum $\text{sum}[v] = \sum_{i=tl}^{tr} A[i]$ covering $\text{len} = tr - tl + 1$ elements, the additive constant $b$ is added to every one of the $\text{len}$ elements, so $\text{sum}[v] \leftarrow a \cdot \text{sum}[v] + b \cdot \text{len}$! However, when composing the lazy tag lazyB[v] = a * lazyB[v] + b, you do not multiply $b$ by len because lazyB[v] is a per-element affine coefficient that will be multiplied by child lengths when pushed down.
Check yourself Question 1 of 4
Concept Check Expressing Range Assignment as an Affine Transformation recall

In a Range Affine Lazy Segment Tree where each lazy tag $(a, b)$ transforms every element $x \leftarrow a \cdot x + b$, how are `rangeAdd(l, r, d)`, `rangeScale(l, r, k)`, and `rangeAssign(l, r, c)` represented as pairs $(a, b)$?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

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

Why does `maxRight(l, pred)` visit at most $O(\log N)$ nodes in total, even though it may recurse into both children when a node straddles $l$ or straddles the threshold $r^*$?

Key Takeaways

Representing range updates as affine functions f(x) = a*x + b unifies rangeAdd (1, d), rangeScale (k, 0), and rangeAssign (0, c) under a single branchless composition rule.
Composing a new affine tag (a2, b2) onto an existing tag (a1, b1) yields (a2 * a1, a2 * b1 + b2), with identity (1, 0).
When updating node v's range sum, multiply the additive offset b by the segment length (tr - tl + 1), while keeping lazyB[v] as a per-element offset.
Walking the segment tree directly in maxRight(l, pred) finds the threshold index in O(log N) time, shaving a log N factor off binary search + rangeSum.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue