Lazy Segment Tree¶
Lazy segment tree is the first real answer when:
- updates hit whole intervals, not single points
- queries still ask for interval aggregates online
- the update effect can be summarized at a node without touching every leaf immediately
It is best learned as an extension of the ordinary Segment Tree, not as a separate black box.
The mindset is:
- keep the same segment-tree interval invariant
- add one
pending update tagper node - postpone pushing that tag to children until a later operation actually needs them
At A Glance¶
- Use when: range updates and range queries are interleaved online, and a whole covered segment can absorb the update through a closed-form node adjustment
- Core worldview: a node can stay correct for its whole segment even if its children are temporarily stale, as long as the pending update tag records what they still owe
- Main tools: segment sums or other mergeable summaries, lazy tags,
apply,push,pull - Typical complexity:
O(log n)per update/query - Main trap: forgetting that “lazy” means
node correct, children deferred, not “skip correctness until later”
Strong contest signals:
- "add
xto every element in[l, r], then answer sums online" - "many long interval updates plus many interval queries"
- "point-update segment tree would touch too many leaves"
- "difference arrays would work only if all queries came after all updates"
Strong anti-cues:
- all updates are known first and you only need the final array -> Difference Arrays
- updates are range add but queries are only point queries -> difference-array / Fenwick routes are lighter
- the statement needs both
range assignandrange add, but you only have a simple additive tag skeleton - the statement now asks for
range chmin/range chmax, so the update family is no longer one simple lazy-tag algebra -> compare Segment Tree Beats - the real structure is a tree, so you first need Euler flattening or HLD before any array lazy tree matters
Prerequisites¶
- Segment Tree
- Difference Arrays
- Fenwick Tree for the lighter neighboring range-update lanes
Helpful neighboring topics:
- Heavy-Light Decomposition when the lazy tree lives on decomposed paths
- Trees once subtree flattening becomes the real reduction
Problem Model And Notation¶
Assume an array:
We want to support:
range_add(l, r, delta)on a half-open interval[l, r)range_sum(l, r)on the same interval convention
The ordinary segment-tree invariant is still:
for the interval [L, R) owned by node v.
The new ingredient is a lazy tag:
meaning:
every element in this segment still owes +lazy[v] before the children are individually up to date
The key point is that the node itself can already be correct even while the children are deferred.
That is the lazy-propagation contract.
Lazy Propagation Playground¶
Apply a range add, then query a smaller interval inside it. The useful thing to watch is not the final number alone, but which nodes absorb the tag immediately, which nodes must push before descent, and which nodes are pulled back from children.
Logical array after applied updates
Visual Reading Guide¶
What to notice:
- a full-cover update does not descend to leaves; it repairs the covered node immediately and records deferred work in its lazy tag
- a partial-overlap query or update forces a
pushbecause the children must become truthful before the algorithm can reason about them individually
Why it matters:
- this is the exact place where lazy propagation stops being "segment tree but faster" and becomes a real invariant discipline
- once this contract is clear, many lazy-tree bugs become easy to spot: either the parent was never made correct, or the children were trusted before a needed push
Code bridge:
applyis the operation that updates one node summary and one lazy tag togetherpushmoves deferred work from a parent to its children before a partial descentpullrebuilds a parent summary from its children after recursion returns- the highlighted tree state is meant to map directly onto
apply / push / pull / range_add / range_sum
Boundary:
- this visual covers only
range add + range sum - once tags stop being one additive number, such as
range assignor multiple update families with precedence rules, the recursion skeleton survives but the tag algebra must be redesigned carefully
From Brute Force To The Right Idea¶
Brute Force¶
Suppose each update says:
- add
deltato every index in[l, r)
and each query asks:
- sum of
[l, r)
The naive approach does:
- range update:
O(r - l) - range query:
O(r - l)
This is too slow once both happen many times.
Why A Basic Segment Tree Is Still Too Eager¶
An ordinary point-update segment tree helps only if each update changes one leaf.
If you try to implement range_add(l, r, delta) by descending to every affected leaf, the cost becomes:
which defeats the whole purpose.
So the real question is:
can one fully covered node be updated in O(1) without touching its children yet?
For range add + range sum, yes.
If a node covers [L, R) and we add delta to every element in that segment, then:
So one node can absorb the whole update immediately.
Why Difference Arrays Are Not Enough¶
Difference arrays also compress range adds, but only in the offline sense:
- you accumulate updates
- then reconstruct later
They break as soon as queries are interleaved with updates.
Lazy segment tree is the online version of the same compression instinct:
- compress the update at the largest fully covered segments
- defer detail work until a later query/update actually needs the children
Core Invariant And Why It Works¶
1. Node Meaning¶
For every node v owning interval [L, R):
sum[v] is the true sum of the whole interval [L, R) after all updates already applied to that node
This must stay correct at all times.
The children are allowed to lag behind if the lazy tag explains that lag.
2. Lazy Tag Meaning¶
The lazy tag lazy[v] = x means:
every leaf in the segment of v still needs +x before the children become individually up to date
Equivalently:
- the parent summary already includes this effect
- the children summaries may not
- pushing the tag later will repair that mismatch
So “lazy” never means “temporarily wrong node value.”
It means:
- parent correct
- descendants deferred
3. Full-Cover Update¶
If the update interval fully covers node v on [L, R), then we do not recurse.
Instead:
and:
Why is that enough?
Because every element of the segment got the same delta, so the aggregate can be updated in closed form, and the children can wait.
This is the entire reason lazy propagation exists.
4. Push¶
When an operation partially overlaps node v, we cannot keep the children stale anymore.
So before descending, we push(v):
- apply the pending tag to the left child
- apply the pending tag to the right child
- clear the parent's tag
After the push:
- both children become correct for their full segments
- the parent tag becomes neutral again
Then recursion can safely continue.
5. Pull¶
After partial recursion returns, recompute:
This is exactly the same pull step as an ordinary segment tree.
So lazy propagation adds one extra discipline:
applypush
but does not replace the ordinary merge invariant.
6. Why Query Still Works¶
Range query uses the same three-way overlap split:
- no overlap -> return identity
- full cover -> use
sum[v]directly - partial overlap ->
push, recurse, merge
This is sound because whenever we use sum[v] directly, it is already the true answer for the whole segment.
Whenever we need finer granularity, push makes the children truthful before we inspect them.
7. Why Complexity Stays Logarithmic¶
Each operation only visits nodes on O(log n) root-to-boundary paths plus a logarithmic number of fully covered segments.
At each visited node, the extra lazy work is O(1):
- maybe one push
- maybe one pull
So:
- range add:
O(log n) - range sum:
O(log n)
The point of laziness is not asymptotically faster than a basic segment tree on point updates.
The point is that range updates stop exploding into leaf-by-leaf work.
Variant Chooser¶
| If the statement asks... | Tag type | First route |
|---|---|---|
| add one delta to every value in a range, then ask sums | additive tag | this page + range-add/sum starter |
assign every value in a range to x, then ask sums |
overwrite tag with precedence | topic first, then custom assign-capable lazy tree |
| both range add and range assign exist | layered tag semantics | do not drop in the add-only starter unchanged |
| affine transforms on a range | function composition tag | ACL lazy_segtree lens, then a specialized implementation |
| subtree/path updates on a tree | lazy tag on a flattened/decomposed array | Euler/HLD first, then lazy tree |
The repo starter for this lane is intentionally narrow:
range addrange sum
That is the cleanest first lazy tree.
Worked Examples¶
Example 1: Why A Fully Covered Node Can Stop Early¶
Suppose a node covers [2, 6) and currently stores:
If the update is:
then the new sum is:
So the node can be fixed immediately in O(1).
There is no need to touch each child yet.
That is the first mental checkpoint for lazy propagation:
full cover means “repair here and defer below”
Example 2: HORRIBLE¶
In HORRIBLE - Horrible Queries, the operations are exactly:
- add
vto every index in[p, q] - ask the sum on
[p, q]
The statement is one-based inclusive, but internally the reusable form is:
- update
[p - 1, q) - query
[p - 1, q)
This is the cleanest flagship note for this lane because:
- the update type matches the starter exactly
- there is no extra assignment tag
- the only real new idea is lazy propagation itself
Example 3: Why Range Updates and Sums Is Already A Next Variant¶
In Range Updates and Sums, one update adds while another assigns.
That changes the tag logic completely:
assign xmust override any earlier pending addadd dafterassign xbecomesassign (x + d)at the same node
So this problem is not just “more tests for the same starter.”
It is the first strong proof that lazy trees are about tag semantics, not only about recursion boilerplate.
Complexity And Tradeoffs¶
For the repo starter range add + range sum:
- build:
O(n) - range add:
O(log n) - range sum:
O(log n) - memory:
O(n)orO(4n)depending on layout
Practical tradeoffs:
| Structure | Best when | Update | Query | Main limitation |
|---|---|---|---|---|
| Difference Arrays | all range adds known first | O(1) per offline mark |
reconstruct later | cannot answer online interval queries |
| Fenwick Tree | additive prefix/range patterns with lighter formulas | O(log n) |
O(log n) |
much narrower update/query family |
| Segment Tree | point updates + general range queries | O(log n) |
O(log n) |
range updates still need leaf work |
| Lazy Segment Tree | online range updates + range queries | O(log n) |
O(log n) |
heavier implementation and tag semantics matter |
Pseudocode¶
apply(v, seg_len, delta):
sum[v] += delta * seg_len
lazy[v] += delta
push(v, left_len, right_len):
if lazy[v] == 0:
return
apply(left child of v, left_len, lazy[v])
apply(right child of v, right_len, lazy[v])
lazy[v] = 0
range_add(v, L, R, ql, qr, delta):
if qr <= L or R <= ql:
return
if ql <= L and R <= qr:
apply(v, R - L, delta)
return
push(v, mid - L, R - mid)
recurse on children
sum[v] = sum[left] + sum[right]
range_sum(v, L, R, ql, qr):
if qr <= L or R <= ql:
return 0
if ql <= L and R <= qr:
return sum[v]
push(v, mid - L, R - mid)
return range_sum(left child) + range_sum(right child)
Implementation Notes¶
- Keep one internal interval convention. The repo starter uses zero-based half-open
[l, r). applyshould be the only place that changes bothsumandlazytogether.pushshould happen only before partial descent, not blindly on every visit.- Use
long long; a range add can multiply by segment length. - For multiple test cases, fully reset both
sumandlazy. - If the statement later adds
range assign, do not bolt it onto an additive tag casually. Redesign the tag semantics first.
Practice Archetypes¶
- direct
range add + range sumonline tasks - subtree add / subtree sum after Euler flattening
- HLD path updates with a lazy base structure
- advanced variants where the real challenge is composing tags correctly
References And Repo Anchors¶
Research sweep refreshed on 2026-04-24.
Primary / official:
Reference:
Repo anchors: