Skip to content

Fix LazySegmentTree propagation complexity bug#1059

Open
Cheshulko wants to merge 1 commit into
TheAlgorithms:masterfrom
Cheshulko:lazy_segment_tree_propagatio_complexity_bug
Open

Fix LazySegmentTree propagation complexity bug#1059
Cheshulko wants to merge 1 commit into
TheAlgorithms:masterfrom
Cheshulko:lazy_segment_tree_propagatio_complexity_bug

Conversation

@Cheshulko

Copy link
Copy Markdown
Contributor

LazySegmentTree propagation complexity bug

The issue

src/problem.rs's LazySegmentTree produced correct results but silently degraded from O(log n) to O(n) per update/query call. The root cause: update_recursive's "fully covered" branch never updated the node's own cached aggregate (tree[idx]) it only stashed the pending delta in lazy[idx] and returned. Since tree[idx] was left stale, the only way to make it correct again was to drain the lazy value all the way down to the leaves and rebuild every ancestor's aggregate bottom-up. That's exactly what propagation did: its only base case was a range of size 1 (a leaf), so every call recursed through the entire subtree under idx instead of stopping after pushing the lazy one level down to idx's two children.

Counter-example

let mut t = LazySegmentTree::from_vec(&vec![0; n], |x, y| x + y); 
t.update(0..n, 1); // O(1): root fully covered, sets lazy[1] 
t.query(0..1); // should be O(log n) ≈ 20 node visits 

That single query(0..1) call triggers propagation(1, 0..n, 0), which has no early exit short of a leaf, so it walks all ~2^21 nodes of the tree instead of the ~20 nodes on the path to element 0. Every ancestor touched during update_recursive was also left with a lazy[idx] = Some(T::default()) "dirty" marker, so this full-tree walk recurred on essentially every query that followed an update turning interleaved update/query workloads effectively quadratic instead of O(log n) per op.

The fix

The one-line "stop after one level" fix isn't sufficient on its own: without knowing the length of the range a node covers, there's no way to fold a pending per-element delta into that node's cached aggregate for all merge functions. Shift-invariant merges like min/max just add the delta; sum needs to add delta * range_length. This can't be derived from a bare merge: fn(T, T) -> T closure it's a semantic property of the specific aggregation, not something inferable from the function's type signature. So the fix adds a second, caller-supplied closure:

apply: fn(T, usize, T) -> T // (node_value, range_len, pending_delta) -> new node_value 

With that in place:

  • apply_node(idx, len, val) immediately folds val into tree[idx] via apply, and merges val into lazy[idx]. This means tree[idx] is always correct the instant an update touches it no need to touch its descendants right away.
  • propagate(idx, element_range) pushes idx's pending lazy to its two direct children only (via apply_node), then clears it O(1), no recursion. This replaces the old propagation, which drained lazy all the way to the leaves. - query_recursive now checks full range coverage before propagating (since tree[idx] is always accurate), and only calls propagate when it actually needs to descend into children.
  • update_recursive's fully-covered branch calls apply_node directly instead of only setting lazy[idx]; the old "dirty marker" (self.lazy[idx] = Some(T::default())) is no longer needed since the aggregate is never allowed to go stale in the first place. Net effect: both query and update now visit O(log n) nodes, each doing O(1) work, matching src/correct.rs's reference implementation (which carries the same three-closure shape: merge, merge_lazy, apply_lazy).

API change

from_vec gained one parameter:

// before 
LazySegmentTree::from_vec(&arr, merge) 
// after 
LazySegmentTree::from_vec(&arr, merge, apply) 

Callers now supply both closures, e.g.:

LazySegmentTree::from_vec(&arr, min, |x, _len, val| x + val); // shift-invariant 
LazySegmentTree::from_vec(&arr, |a, b| a + b, |x, len, val| x + val * len as i64); // sum 

Tests added

  • test_update_segments_min / test_update_segments_max: functional checks mirroring the existing test_update_segments (sum), verifying correctness after overlapping range updates for the two other merges.
  • test_large_array_min / test_large_array_max / test_large_array_sum: build a ~1M-element (2^20) array and apply 1000 single-index updates scattered across it, then assert query results exercises the fixed propagate/apply_node path at real depth/scale without relying on wall-clock timing.

Codeforces testing

References

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

  • I ran bellow commands using the latest version of rust nightly.
  • I ran cargo clippy --all -- -D warnings just before my last commit and fixed any issue that was found.
  • I ran cargo fmt just before my last commit.
  • I ran cargo test just before my last commit and all tests passed.
  • I checked COUNTRIBUTING.md and my code follows its guidelines.

@Cheshulko
Cheshulko requested a review from imp2002 as a code owner July 21, 2026 12:50
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.90%. Comparing base (9b296e9) to head (2a8d78a).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1059   +/-   ##
=======================================
  Coverage   95.89%   95.90%           
=======================================
  Files         396      396           
  Lines       30440    30465   +25     
=======================================
+ Hits        29191    29217   +26     
+ Misses       1249     1248    -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants