AlgoMaster Logo

Range Sum Query - Mutable

Last Updated: April 4, 2026

medium
5 min read

Understanding the Problem

We need a data structure that supports two operations on an array: point updates (change one element) and range sum queries (sum a contiguous subarray). The catch is that both operations are interleaved, so we can't just precompute all answers upfront.

If updates never happened, a prefix sum array would give us O(1) range queries after O(n) preprocessing. But every time we update a single element, the prefix sum becomes stale, and rebuilding it costs O(n). On the flip side, if queries never happened, we'd just store the array and update in O(1). The problem forces us to handle both efficiently, and that tension between fast updates and fast queries is what makes this interesting.

The key insight is that we need a data structure that can decompose range operations into smaller pieces, updating only the relevant pieces when an element changes. Both Segment Trees and Binary Indexed Trees (Fenwick Trees) achieve this by splitting the array into hierarchical segments.

Key Constraints:

  • 1 <= nums.length <= 3 * 10^4 → With n up to 30,000, an O(n) per operation approach gives 30,000 30,000 = 9 10^8 operations in the worst case. That's too slow. We need something better than linear per operation.
  • At most 3 * 10^4 calls to update and sumRange → Combined with n up to 30,000, the total work budget is roughly 30,000 operations. If each operation is O(log n), that's about 30,000 * 15 = 450,000 operations total, which is very comfortable.
  • -100 <= nums[i] <= 100 → Small values, no overflow concerns with 32-bit integers for sums.

Approach 1: Brute Force (Prefix Sum with Rebuild)

Intuition

The most natural first thought: use a prefix sum array to answer range queries in O(1). When an update happens, just rebuild the prefix sum from the changed index onward.

For sumRange(left, right), the answer is prefix[right + 1] - prefix[left]. For update(index, val), we update the original array, then recompute the prefix sums from index to the end.

This is a perfectly reasonable starting point, and for small arrays with few updates, it works fine.

Algorithm

  1. During initialization, build a prefix sum array where prefix[i] = sum of nums[0..i-1].
  2. For sumRange(left, right), return prefix[right + 1] - prefix[left].
  3. For update(index, val), compute the difference delta = val - nums[index], update nums[index] = val, then add delta to every prefix sum from prefix[index + 1] through prefix[n].

Example Walkthrough

1Init: prefix = [0, 1, 4, 9]
0
1
1
1
3
3
2
5
5
1/5

Code

Every update call walks through the prefix array from the changed index to the end. What if we could decompose the array into smaller segments, so that updating one element only affects a few segments?

Approach 2: Square Root Decomposition

Intuition

Instead of maintaining one giant prefix sum, split the array into blocks of size roughly sqrt(n). Each block stores its own precomputed sum. When we update an element, we only need to adjust the sum for that one block, which is O(1). When we query a range, we sum up the complete blocks in the middle and manually add the partial blocks at the edges.

This gives us O(1) updates and O(sqrt(n)) queries. Both operations are sub-linear, which is a big improvement over the brute force.

Algorithm

  1. Choose a block size b = floor(sqrt(n)).
  2. Create an array blockSum where blockSum[k] = sum of elements in block k.
  3. For update(index, val): compute which block the index belongs to (index / b), update the original array, and adjust the block sum by the difference.
  4. For sumRange(left, right): add up partial elements in the first and last blocks, then add complete block sums for all blocks fully contained in the range.

Example Walkthrough

1Init: Block 0=[1,3] sum=4, Block 1=[5,7] sum=12, Block 2=[2,4] sum=6
0
1
1
3
block 0=4
2
5
3
7
block 1=12
4
2
5
4
block 2=6
1/7

Code

Each query still iterates through up to O(sqrt(n)) elements. What if we used a tree-based decomposition where each level halves the range, giving O(log n) per operation?

Approach 3: Binary Indexed Tree (Fenwick Tree)

Intuition

A Binary Indexed Tree (BIT), also called a Fenwick Tree, uses the binary representation of indices to achieve O(log n) for both updates and prefix sum queries. Each position i in the BIT stores the sum of a specific range of elements, determined by the lowest set bit of i.

For index 6 (binary 110), the lowest set bit is 2, so BIT[6] stores the sum of 2 elements. For index 8 (binary 1000), the lowest set bit is 8, so BIT[8] stores the sum of 8 elements. To get a prefix sum, you hop through at most log(n) positions. To update, you propagate the change to at most log(n) ancestors.

To answer sumRange(left, right), we compute prefixSum(right) - prefixSum(left - 1).

Algorithm

  1. Create a BIT array of size n + 1 (1-indexed).
  2. Initialize it by inserting each element one by one using the update operation.
  3. For update(index, val): compute delta = val - nums[index], update nums[index], then propagate delta up through the BIT by repeatedly adding the lowest set bit to the index.
  4. For sumRange(left, right): compute query(right + 1) - query(left), where query(i) sums BIT values by repeatedly stripping the lowest set bit from i.

Example Walkthrough

1Original array: nums = [1, 3, 5]
0
1
1
3
2
5
1/6

Code

The Fenwick Tree is already O(log n), but it only supports sum queries. For a more general structure that can handle range min/max or lazy propagation, we can use a Segment Tree.

Approach 4: Segment Tree

Intuition

A Segment Tree builds a complete binary tree where each node stores the sum of a contiguous range. The root stores the total sum, its two children store sums of the left and right halves, and so on recursively until each leaf holds a single element.

For updates, we change the leaf and walk up to the root, recalculating each ancestor's sum. For queries, if the query range fully covers a node's range, we return that node's value. Otherwise, we recurse into the children that overlap with the query range.

Algorithm

  1. Build a segment tree array of size 4n (to account for the tree structure).
  2. Recursively build the tree: each leaf holds one array element, each internal node holds the sum of its children.
  3. For update(index, val): find the leaf for that index, update it, then propagate the change up to the root by recalculating parent sums.
  4. For sumRange(left, right): recursively query the tree, combining results from nodes whose ranges overlap with [left, right].

Example Walkthrough

1Build segment tree from nums = [1, 3, 5, 2, 4]
0
1
1
3
2
5
3
2
4
4
1/7

Code