Range Sum Query - Mutable (LeetCode 307)
You will see how a Fenwick tree answers range sums and point updates in O(log N) each by storing blocks sized by the lowest set bit.
You get an integer array nums, followed by a mix of two kinds of requests in any order: change one element, or report the sum of a contiguous stretch of elements.
Build the NumArray class:
NumArray(nums)stores the array.update(index, val)setsnums[index]toval.sumRange(left, right)returnsnums[left] + nums[left + 1] + ... + nums[right], both ends included (left <= right).
Worked Examples
["NumArray", "sumRange", "update", "sumRange"]
[[[1, 3, 5]], [0, 2], [1, 2], [0, 2]][null, 9, null, 8]⚖️Formal Constraints & Bounds
1 <= nums.length <= 3 * 104-100 <= nums[i] <= 1000 <= index < nums.length-100 <= val <= 1000 <= left <= right < nums.lengthAt most
3 * 104calls will be made toupdateandsumRange.
Why It Works & Core Invariant
Store sums of blocks whose length is the index's lowest set bit: a prefix is the sum of O(log N) disjoint blocks, and a changed position belongs to O(log N) blocks, so both operations walk one block per bit.
Real-World Scenario & Production Applications
Live dashboards and leaderboards that keep running totals while individual values keep changing: bucketed counters (requests per second, sales per day) where a single bucket is corrected and a range total is read on every page load. A plain prefix array would need an O(N) rebuild after every change.
Step-by-Step Execution Trace Table
nums = [1, 3, 5] (n = 3). Each value is added at i = index + 1; i & (-i) decides the jump.
| Call | Walk of i | tree[1..3] after | Returns |
|---|---|---|---|
build: update(1, 1) | 1 → 2 → 4 (stop, > 3) | [1, 1, 0] | |
build: update(2, 3) | 2 → 4 (stop) | [1, 4, 0] | |
build: update(3, 5) | 3 → 4 (stop) | [1, 4, 5] | |
sumRange(0, 2) = query(3) - query(0) | query(3): tree[3] + tree[2] = 5 + 4 (3 → 2 → 0) | [1, 4, 5] | 9 |
update(1, 2): add 2 - 3 = -1 at i = 2 | 2 → 4 (stop) | [1, 3, 5] | null |
sumRange(0, 2) | query(3): 5 + 3 = 8 | [1, 3, 5] | 8 |
| 1 | `tree[i]` holds the sum of the `i & (-i)` positions that end at `i`, so any prefix is a few whole blocks and any position sits in a few blocks. |
| 2 | `query(i)` adds `tree[i]` and steps `i -= i & (-i)` down to 0; `update(i, delta)` adds `delta` to `tree[i]` and steps `i += i & (-i)` up past `n`. |
| 3 | `NumArray`: keep `self.nums`, build by calling `update(index + 1, val)` for each value, `update` adds `val - self.nums[index]`, `sumRange` returns `range_query(left + 1, right + 1)`. |
| 4 | The trap: the tree is 1-indexed. `i & (-i)` is 0 when `i` is 0, so `update(0, ...)` never moves and loops forever; shift every index up by one. |
Target: Range Sum Query - Mutable (LeetCode 307). Slot 0 is unused: `i & (-i)` is 0 at i = 0, so the walks could never move from there.
Trie: root-to-node path encodes common prefix; DSU: find(u) with path compression flattens tree so root is direct parent.
Trie: traverse word char-by-char in O(L); DSU: if find(u) != find(v): union(u, v) in O(alpha(N)).
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
A prefix-sum array answers range sums in but needs an rebuild after every change; a plain array updates in but sums in . A Fenwick tree (Binary Indexed Tree) meets in the middle: both operations take . Slot tree[i] stores the sum of a block of positions that ends at i and is i & (-i) long (the value of i's lowest set bit).
🏟️ The Analogy: Receipts Filed by Month, Quarter and Year
An accountant keeps receipts in folders of different sizes: some cover one month, some two, some four, some eight, each ending at a certain month. To total January to November, you add a handful of folders that together cover exactly those months. When a March receipt is corrected, you fix every folder that contains March: a handful again.
🪄 The Mathematical Harmony / Magic Trick
def query(self, i): # sum of positions 1..i total = 0 while i > 0: total += self.tree[i] i -= i & (-i) # jump to just before this block return total def update(self, i, delta): # add delta at position i while i <= self.n: self.tree[i] += delta i += i & (-i) # jump to the next block that also contains i Removing the lowest set bit lands just before the block tree[i] covers, so the blocks query adds never overlap and cover exactly 1..i. Adding it reaches the next bigger block that contains i. Both loops touch one block per bit: .
💡 Summary
The tree is 1-indexed: i & (-i) is 0 when i is 0, so position 0 would never move. NumArray shifts every index up by one, keeps the current values to turn "set to val" into "add val - old", and answers sumRange(left, right) as query(right + 1) - query(left).
Using index 0 in the tree:
i & (-i)is0wheniis0, soupdate(0, delta)adds forever without moving. Shift every LeetCode index by one:update(index + 1, ...).Adding
valinstead of the change:update(index, val)sets a value, but the tree can only add. Addval - self.nums[index]and then storeval, or every repeated update double-counts.Off-by-one in
sumRange: with 1-indexed slots the sum ofnums[left..right]isquery(right + 1) - query(left), which isrange_query(left + 1, right + 1).Rebuilding a prefix array on every update: correct but
O(N)per update; with3 * 104calls on3 * 104elements that is about 10^9 steps.
4-Phase Thought Process Model
You will see how a senior engineer picks a Fenwick tree for point updates plus range sums, and names its indexing trap.
Pattern Recognition Signals
The 10-second spot
"Change one element" mixed with "sum of a contiguous stretch", many times and in any order: a prefix array alone rebuilds in O(N) per change, and a plain array sums in O(N). Point updates plus prefix-based range sums are the exact job of a Fenwick Tree.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
self.tree[i] always equals the sum of the values at positions i - (i & -i) + 1 through i; update keeps it true by walking i += i & (-i), and query(i) adds disjoint blocks walking i -= i & (-i).
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
self.fenwick.update(index + 1, val): the tree is 1-indexed;i & (-i)is 0 ati = 0, so position 0 would loop forever.val - self.nums[index]: the tree adds, so an update must add the change and then store the new value.range_query(left + 1, right + 1)=query(right + 1) - query(left): both ends shifted by one.
The 60-Second Interview Pitch
Say this out loud before you type a single line
I'd use a Fenwick Tree, because I need point updates and range sums mixed together. Slot i of the tree stores the sum of a block ending at i whose length is i's lowest set bit. A prefix sum walks down by removing that bit each time, adding whole blocks that never overlap; an update walks up by adding the bit, fixing every block that contains the position. Each walk touches one block per bit, so both are
O(log N). In NumArray I keep the current values so an update can add val minus the old value, since the tree only adds, and a range sum is the prefix at right plus one minus the prefix at left. The trap is indexing: the tree is 1-indexed, because at zero the lowest set bit is zero and the loop would never move, so I shift every index up by one. Building costsO(N log N)and space isO(N).
So: two prefix sums per range, one walk per bit, 1-indexed slots, and add val - old on update.
Complexity & Mathematical Proof
O(log N) update & query
Look at the loops: update runs i += i & (-i) until i > n; each step moves the lowest set bit of i at least one place higher, so it runs at most about log2(N) + 1 times. query runs i -= i & (-i) until i == 0; each step removes one set bit, so it runs at most log2(N) + 1 times. sumRange is two queries and NumArray.update is one tree update: O(log N) each. Building NumArray makes N tree updates: O(N log N) once.
O(N)
self.tree has N + 1 slots and self.nums keeps N current values: O(N). The loops are iterative, so there is no recursion stack.
T(update) = T(query) = O(log N); T(sumRange) = 2 · O(log N); build = N · O(log N)
Look at the loops: update runs i += i & (-i) until i > n; each step moves the lowest set bit of i at least one place higher, so it runs at most about log2(N) + 1 times. query runs i -= i & (-i) until i == 0; each step removes one set bit, so it runs at most log2(N) + 1 times. sumRange is two queries and NumArray.update is one tree update: O(log N) each. Building NumArray makes N tree updates: O(N log N) once.
Derivation Progression
N × O(log N)
__init__ calls self.fenwick.update(index + 1, val) once per value.
O(log N)
i += i & (-i) climbs at most one step per bit of N.
O(log N)
i -= i & (-i) clears one set bit per step.
2 × O(log N)
range_query is query(right) - query(left - 1).
Variable Definitions
Length of nums
i & (-i)
The lowest set bit of i: the length of the block tree[i] covers
Memory Architecture & Bounds
O(1): both walks are loops
O(N): tree (N + 1 slots) and nums
O(1) per call
Boundary Best / Worst Cases
steps when i is a large power of two for update, or has one set bit for query
per operation; to build
per operation
Recurrence Tree Topology
Senior SWE Deconstruction & Hardware Caveats
Triggers: "change one element", "sum of a contiguous stretch", "in any order". Point updates mixed with range sums: Fenwick Tree (two prefix sums per range, O(log N) each).
values in and at most calls. Sums stay within . Naive: up to steps; Fenwick: about .
Index 0 loops forever. In a concurrent service, an update is several writes (one per block), so readers must not see a half-applied update: guard the walk with a lock or apply updates from a single writer.
Core Algorithmic State Invariants
`self.tree[i]` always equals the sum of the values at positions `i - (i & -i) + 1` through `i`: a block ending at `i` whose length is `i`'s lowest set bit.
`query` adds `tree[i]` and removes the lowest set bit, so its blocks never overlap and cover exactly `1..i`; `update` adds the lowest set bit to reach every larger block containing `i`.
Slot 0 is never used (`0 & -0 == 0` would loop forever), so LeetCode index `k` lives at slot `k + 1`. Each walk takes at most one step per bit: O(log N) per call, O(N) space.
Range Sum Query - Mutable (LeetCode 307)
You will see how a Fenwick tree answers range sums and point updates in O(log N) each by storing blocks sized by the lowest set bit.
You get an integer array nums, followed by a mix of two kinds of requests in any order: change one element, or report the sum of a contiguous stretch of elements.
Build the NumArray class:
NumArray(nums)stores the array.update(index, val)setsnums[index]toval.sumRange(left, right)returnsnums[left] + nums[left + 1] + ... + nums[right], both ends included (left <= right).
Worked Examples
["NumArray", "sumRange", "update", "sumRange"]
[[[1, 3, 5]], [0, 2], [1, 2], [0, 2]][null, 9, null, 8]⚖️Formal Constraints & Bounds
1 <= nums.length <= 3 * 104-100 <= nums[i] <= 1000 <= index < nums.length-100 <= val <= 1000 <= left <= right < nums.lengthAt most
3 * 104calls will be made toupdateandsumRange.
Why It Works & Core Invariant
Store sums of blocks whose length is the index's lowest set bit: a prefix is the sum of O(log N) disjoint blocks, and a changed position belongs to O(log N) blocks, so both operations walk one block per bit.
Real-World Scenario & Production Applications
Live dashboards and leaderboards that keep running totals while individual values keep changing: bucketed counters (requests per second, sales per day) where a single bucket is corrected and a range total is read on every page load. A plain prefix array would need an O(N) rebuild after every change.
Step-by-Step Execution Trace Table
nums = [1, 3, 5] (n = 3). Each value is added at i = index + 1; i & (-i) decides the jump.
| Call | Walk of i | tree[1..3] after | Returns |
|---|---|---|---|
build: update(1, 1) | 1 → 2 → 4 (stop, > 3) | [1, 1, 0] | |
build: update(2, 3) | 2 → 4 (stop) | [1, 4, 0] | |
build: update(3, 5) | 3 → 4 (stop) | [1, 4, 5] | |
sumRange(0, 2) = query(3) - query(0) | query(3): tree[3] + tree[2] = 5 + 4 (3 → 2 → 0) | [1, 4, 5] | 9 |
update(1, 2): add 2 - 3 = -1 at i = 2 | 2 → 4 (stop) | [1, 3, 5] | null |
sumRange(0, 2) | query(3): 5 + 3 = 8 | [1, 3, 5] | 8 |
| 1 | `tree[i]` holds the sum of the `i & (-i)` positions that end at `i`, so any prefix is a few whole blocks and any position sits in a few blocks. |
| 2 | `query(i)` adds `tree[i]` and steps `i -= i & (-i)` down to 0; `update(i, delta)` adds `delta` to `tree[i]` and steps `i += i & (-i)` up past `n`. |
| 3 | `NumArray`: keep `self.nums`, build by calling `update(index + 1, val)` for each value, `update` adds `val - self.nums[index]`, `sumRange` returns `range_query(left + 1, right + 1)`. |
| 4 | The trap: the tree is 1-indexed. `i & (-i)` is 0 when `i` is 0, so `update(0, ...)` never moves and loops forever; shift every index up by one. |
Target: Range Sum Query - Mutable (LeetCode 307). Slot 0 is unused: `i & (-i)` is 0 at i = 0, so the walks could never move from there.
Trie: root-to-node path encodes common prefix; DSU: find(u) with path compression flattens tree so root is direct parent.
Trie: traverse word char-by-char in O(L); DSU: if find(u) != find(v): union(u, v) in O(alpha(N)).
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
A prefix-sum array answers range sums in but needs an rebuild after every change; a plain array updates in but sums in . A Fenwick tree (Binary Indexed Tree) meets in the middle: both operations take . Slot tree[i] stores the sum of a block of positions that ends at i and is i & (-i) long (the value of i's lowest set bit).
🏟️ The Analogy: Receipts Filed by Month, Quarter and Year
An accountant keeps receipts in folders of different sizes: some cover one month, some two, some four, some eight, each ending at a certain month. To total January to November, you add a handful of folders that together cover exactly those months. When a March receipt is corrected, you fix every folder that contains March: a handful again.
🪄 The Mathematical Harmony / Magic Trick
def query(self, i): # sum of positions 1..i total = 0 while i > 0: total += self.tree[i] i -= i & (-i) # jump to just before this block return total def update(self, i, delta): # add delta at position i while i <= self.n: self.tree[i] += delta i += i & (-i) # jump to the next block that also contains i Removing the lowest set bit lands just before the block tree[i] covers, so the blocks query adds never overlap and cover exactly 1..i. Adding it reaches the next bigger block that contains i. Both loops touch one block per bit: .
💡 Summary
The tree is 1-indexed: i & (-i) is 0 when i is 0, so position 0 would never move. NumArray shifts every index up by one, keeps the current values to turn "set to val" into "add val - old", and answers sumRange(left, right) as query(right + 1) - query(left).
Using index 0 in the tree:
i & (-i)is0wheniis0, soupdate(0, delta)adds forever without moving. Shift every LeetCode index by one:update(index + 1, ...).Adding
valinstead of the change:update(index, val)sets a value, but the tree can only add. Addval - self.nums[index]and then storeval, or every repeated update double-counts.Off-by-one in
sumRange: with 1-indexed slots the sum ofnums[left..right]isquery(right + 1) - query(left), which isrange_query(left + 1, right + 1).Rebuilding a prefix array on every update: correct but
O(N)per update; with3 * 104calls on3 * 104elements that is about 10^9 steps.
4-Phase Thought Process Model
You will see how a senior engineer picks a Fenwick tree for point updates plus range sums, and names its indexing trap.
Pattern Recognition Signals
The 10-second spot
"Change one element" mixed with "sum of a contiguous stretch", many times and in any order: a prefix array alone rebuilds in O(N) per change, and a plain array sums in O(N). Point updates plus prefix-based range sums are the exact job of a Fenwick Tree.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
self.tree[i] always equals the sum of the values at positions i - (i & -i) + 1 through i; update keeps it true by walking i += i & (-i), and query(i) adds disjoint blocks walking i -= i & (-i).
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
self.fenwick.update(index + 1, val): the tree is 1-indexed;i & (-i)is 0 ati = 0, so position 0 would loop forever.val - self.nums[index]: the tree adds, so an update must add the change and then store the new value.range_query(left + 1, right + 1)=query(right + 1) - query(left): both ends shifted by one.
The 60-Second Interview Pitch
Say this out loud before you type a single line
I'd use a Fenwick Tree, because I need point updates and range sums mixed together. Slot i of the tree stores the sum of a block ending at i whose length is i's lowest set bit. A prefix sum walks down by removing that bit each time, adding whole blocks that never overlap; an update walks up by adding the bit, fixing every block that contains the position. Each walk touches one block per bit, so both are
O(log N). In NumArray I keep the current values so an update can add val minus the old value, since the tree only adds, and a range sum is the prefix at right plus one minus the prefix at left. The trap is indexing: the tree is 1-indexed, because at zero the lowest set bit is zero and the loop would never move, so I shift every index up by one. Building costsO(N log N)and space isO(N).
So: two prefix sums per range, one walk per bit, 1-indexed slots, and add val - old on update.
Complexity & Mathematical Proof
O(log N) update & query
Look at the loops: update runs i += i & (-i) until i > n; each step moves the lowest set bit of i at least one place higher, so it runs at most about log2(N) + 1 times. query runs i -= i & (-i) until i == 0; each step removes one set bit, so it runs at most log2(N) + 1 times. sumRange is two queries and NumArray.update is one tree update: O(log N) each. Building NumArray makes N tree updates: O(N log N) once.
O(N)
self.tree has N + 1 slots and self.nums keeps N current values: O(N). The loops are iterative, so there is no recursion stack.
T(update) = T(query) = O(log N); T(sumRange) = 2 · O(log N); build = N · O(log N)
Look at the loops: update runs i += i & (-i) until i > n; each step moves the lowest set bit of i at least one place higher, so it runs at most about log2(N) + 1 times. query runs i -= i & (-i) until i == 0; each step removes one set bit, so it runs at most log2(N) + 1 times. sumRange is two queries and NumArray.update is one tree update: O(log N) each. Building NumArray makes N tree updates: O(N log N) once.
Derivation Progression
N × O(log N)
__init__ calls self.fenwick.update(index + 1, val) once per value.
O(log N)
i += i & (-i) climbs at most one step per bit of N.
O(log N)
i -= i & (-i) clears one set bit per step.
2 × O(log N)
range_query is query(right) - query(left - 1).
Variable Definitions
Length of nums
i & (-i)
The lowest set bit of i: the length of the block tree[i] covers
Memory Architecture & Bounds
O(1): both walks are loops
O(N): tree (N + 1 slots) and nums
O(1) per call
Boundary Best / Worst Cases
steps when i is a large power of two for update, or has one set bit for query
per operation; to build
per operation
Recurrence Tree Topology
Senior SWE Deconstruction & Hardware Caveats
Triggers: "change one element", "sum of a contiguous stretch", "in any order". Point updates mixed with range sums: Fenwick Tree (two prefix sums per range, O(log N) each).
values in and at most calls. Sums stay within . Naive: up to steps; Fenwick: about .
Index 0 loops forever. In a concurrent service, an update is several writes (one per block), so readers must not see a half-applied update: guard the walk with a lock or apply updates from a single writer.
Core Algorithmic State Invariants
`self.tree[i]` always equals the sum of the values at positions `i - (i & -i) + 1` through `i`: a block ending at `i` whose length is `i`'s lowest set bit.
`query` adds `tree[i]` and removes the lowest set bit, so its blocks never overlap and cover exactly `1..i`; `update` adds the lowest set bit to reach every larger block containing `i`.
Slot 0 is never used (`0 & -0 == 0` would loop forever), so LeetCode index `k` lives at slot `k + 1`. Each walk takes at most one step per bit: O(log N) per call, O(N) space.
| Canonical Invariant | Concrete Code | Engineering Rationale |
|---|---|---|
| One slot per position, 1-indexed | self.tree = [0] * (n + 1) | Slot 0 is unused: `i & (-i)` is 0 at i = 0, so the walks could never move from there. |
| Point update: fix every block that contains i | while i <= self.n:
self.tree[i] += delta
i += i & (-i) | Adding the lowest set bit jumps to the next larger block that also covers position i. |
| Prefix query: add disjoint blocks that cover 1..i | while i > 0:
total += self.tree[i]
i -= i & (-i) | Removing the lowest set bit jumps to just before the current block, so no position is counted twice. |
| Shift 0-indexed positions to 1-indexed slots | self.fenwick.update(index + 1, val) | LeetCode's index 0 lives at slot 1. |
| Turn 'set to val' into 'add a change' | self.fenwick.update(index + 1, val - self.nums[index])
self.nums[index] = val | The tree only adds; keeping the current values gives the difference to add. |
| Range sum from two prefix sums | return self.fenwick.range_query(left + 1, right + 1) | `range_query(l, r) = query(r) - query(l - 1)`: everything before `left` cancels. |