Fix LazySegmentTree propagation complexity bug#1059
Open
Cheshulko wants to merge 1 commit into
Open
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
LazySegmentTree
propagationcomplexity bugThe issue
src/problem.rs'sLazySegmentTreeproduced correct results but silently degraded from O(log n) to O(n) perupdate/querycall. 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 inlazy[idx]and returned. Sincetree[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 whatpropagationdid: its only base case was a range of size 1 (a leaf), so every call recursed through the entire subtree underidxinstead of stopping after pushing the lazy one level down toidx's two children.Counter-example
That single
query(0..1)call triggerspropagation(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 duringupdate_recursivewas also left with alazy[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/maxjust add the delta;sumneeds to adddelta * range_length. This can't be derived from a baremerge: fn(T, T) -> Tclosure 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:With that in place:
apply_node(idx, len, val)immediately foldsvalintotree[idx]viaapply, and mergesvalintolazy[idx]. This meanstree[idx]is always correct the instant an update touches it no need to touch its descendants right away.propagate(idx, element_range)pushesidx's pending lazy to its two direct children only (viaapply_node), then clears it O(1), no recursion. This replaces the oldpropagation, which drained lazy all the way to the leaves. -query_recursivenow checks full range coverage before propagating (sincetree[idx]is always accurate), and only callspropagatewhen it actually needs to descend into children.update_recursive's fully-covered branch callsapply_nodedirectly instead of only settinglazy[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: bothqueryandupdatenow visit O(log n) nodes, each doing O(1) work, matchingsrc/correct.rs's reference implementation (which carries the same three-closure shape:merge,merge_lazy,apply_lazy).API change
from_vecgained one parameter:Callers now supply both closures, e.g.:
Tests added
test_update_segments_min/test_update_segments_max: functional checks mirroring the existingtest_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 fixedpropagate/apply_nodepath at real depth/scale without relying on wall-clock timing.Codeforces testing
References
Type of change
Checklist:
cargo clippy --all -- -D warningsjust before my last commit and fixed any issue that was found.cargo fmtjust before my last commit.cargo testjust before my last commit and all tests passed.COUNTRIBUTING.mdand my code follows its guidelines.