Skip to content

Commit 875082a

Browse files
Fix maximum Fenwick tree range values after updates (#15485)
* Fix maximum Fenwick tree range values after updates * Add Fenwick regression doctests and 2000-item benchmark Preserve all existing doctests while covering lost range maxima and decreases. Add an optional timeit benchmark for update and query workloads, as requested in #15485. * Avoid unnecessary maximum Fenwick recomputation Propagate only changed maxima and rebuild necessary buckets directly from disjoint child buckets. Preserve arbitrary overwrites, regression doctests and the benchmark. --------- Co-authored-by: Christian Clauss <cclauss@me.com>
1 parent 0465a5f commit 875082a

1 file changed

Lines changed: 78 additions & 3 deletions

File tree

‎data_structures/binary_tree/maximum_fenwick_tree.py‎

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,26 @@ class MaxFenwickTree:
3434
>>> ft.update(0, 1000)
3535
>>> ft.query(0, 1)
3636
1000
37+
38+
Updating a smaller sibling preserves the maximum, including after decreases.
39+
>>> ft = MaxFenwickTree(8)
40+
>>> ft.update(4, 20)
41+
>>> ft.update(5, 1)
42+
>>> ft.query(0, 6)
43+
20
44+
>>> ft.update(4, 0)
45+
>>> ft.query(0, 6)
46+
1
47+
>>> ft.update(5, 0)
48+
>>> ft.query(0, 6)
49+
0
50+
51+
Updating a child must also preserve the ancestor's own array value.
52+
>>> ft = MaxFenwickTree(8)
53+
>>> ft.update(5, 100)
54+
>>> ft.update(4, 0)
55+
>>> ft.query(0, 6)
56+
100
3757
"""
3858

3959
def __init__(self, size: int) -> None:
@@ -75,13 +95,32 @@ def update(self, index: int, value: int) -> None:
7595
Returns:
7696
None
7797
"""
98+
old_value = self.arr[index]
99+
if value == old_value:
100+
return
78101
self.arr[index] = value
79102
while index < self.size:
80-
current_left_border = self.get_prev(index) + 1
81-
if current_left_border == index:
103+
old_maximum = self.tree[index]
104+
if value > old_maximum:
82105
self.tree[index] = value
106+
elif old_value == old_maximum:
107+
current_left_border = self.get_prev(index) + 1
108+
maximum = self.arr[index]
109+
if current_left_border != index:
110+
maximum = max(0, maximum)
111+
child = index - 1
112+
# These disjoint child buckets cover the rest of this bucket.
113+
while child >= current_left_border:
114+
maximum = max(maximum, self.tree[child])
115+
if maximum == old_maximum:
116+
break
117+
child = self.get_prev(child)
118+
self.tree[index] = maximum
119+
if maximum == old_maximum:
120+
break
83121
else:
84-
self.tree[index] = max(value, current_left_border, index)
122+
# An unchanged bucket maximum leaves all its ancestors unchanged.
123+
break
85124
index = self.get_next(index)
86125

87126
def query(self, left: int, right: int) -> int:
@@ -110,5 +149,41 @@ def query(self, left: int, right: int) -> int:
110149

111150
if __name__ == "__main__":
112151
import doctest
152+
import sys
153+
from timeit import repeat
113154

114155
doctest.testmod()
156+
157+
# Run with --benchmark on each revision to compare the same 2,000-item workload.
158+
if "--benchmark" in sys.argv:
159+
size = 2000
160+
values = [(index * 97) % size for index in range(size)]
161+
updates = [(index, (index * 37) % size) for index in range(size)]
162+
updates += [(index, values[index]) for index in reversed(range(size))]
163+
queries = [(left, size) for left in range(size)]
164+
165+
# Build correct query buckets outside the timer, even on the buggy revision.
166+
query_setup = """
167+
tree = MaxFenwickTree(size)
168+
tree.arr = values[:]
169+
tree.tree = [
170+
max(values[tree.get_prev(index) + 1 : index + 1])
171+
for index in range(size)
172+
]
173+
"""
174+
for operation, statement, setup in (
175+
(
176+
"4000 updates",
177+
"for index, value in updates: tree.update(index, value)",
178+
"tree = MaxFenwickTree(size)",
179+
),
180+
(
181+
"2000 queries",
182+
"for left, right in queries: tree.query(left, right)",
183+
query_setup,
184+
),
185+
):
186+
timings = repeat(
187+
statement, setup=setup, repeat=5, number=1, globals=globals()
188+
)
189+
print(f"{size} items, {operation}: {min(timings):.6f} seconds (best of 5)")

0 commit comments

Comments
 (0)