Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclusive.
The update(i, val) function modifies nums by updating the element at index i to val.Example:
Given nums = [1, 3, 5] sumRange(0, 2) -> 9 update(1, 2) sumRange(0, 2) -> 8
Note:
This article is for intermediate level readers. It introduces the following concepts: Range sum query, Sqrt decomposition, Segment tree.
Algorithm
A trivial solution for Range Sum Query - RSQ(i, j)
is to iterate the array from index to and sum each element.
Java
private int[] nums; public int sumRange(int i, int j) { int sum = 0; for (int l = i; l <= j; l++) { sum += data[l]; } return sum; } public int update(int i, int val) { nums[i] = val; } // Time Limit Exceeded
Complexity Analysis
Time complexity : - range sum query, - update query
For range sum query we access each element from the array for constant time and in the worst case we access elements. Therefore time complexity is . Time complexity of update query is .
Space complexity : .
Intuition
The idea is to split the array in blocks with length of . Then we calculate the sum of each block and store it in auxiliary memory b
.
To query RSQ(i, j)
, we will add the sums of all the blocks lying inside and those that partially overlap with range .
Algorithm
Figure 1. Range sum query using SQRT decomposition.
In the example above, the array nums
's length is 9
, which is split into blocks of size . To get RSQ(1, 7)
we add b[1]
. It stores the sum of range [3, 5]
and partially sums from block 0
and block 2
, which are overlapping boundary blocks.
Java
private int[] b; private int len; private int[] nums; public NumArray(int[] nums) { this.nums = nums; double l = Math.sqrt(nums.length); len = (int) Math.ceil(nums.length/l); b = new int [len]; for (int i = 0; i < nums.length; i++) b[i / len] += nums[i]; } public int sumRange(int i, int j) { int sum = 0; int startBlock = i / len; int endBlock = j / len; if (startBlock == endBlock) { for (int k = i; k <= j; k++) sum += nums[k]; } else { for (int k = i; k <= (startBlock + 1) * len - 1; k++) sum += nums[k]; for (int k = startBlock + 1; k <= endBlock - 1; k++) sum += b[k]; for (int k = endBlock * len; k <= j; k++) sum += nums[k]; } return sum; } public void update(int i, int val) { int b_l = i / len; b[b_l] = b[b_l] - nums[i] + val; nums[i] = val; } // Accepted
Complexity Analysis
Time complexity : - preprocessing, - range sum query, - update query
For range sum query in the worst-case scenario we have to sum approximately elements. In this case the range includes blocks, which total sum costs operations. In addition to this we have to add the sum of the two boundary blocks. This takes another operations. The total amount of operations is around .
Space complexity : .
We need additional memory to store all block sums.
Algorithm
Segment tree is a very flexible data structure, because it is used to solve numerous range query problems like finding minimum, maximum, sum, greatest common divisor, least common denominator in array in logarithmic time.
Figure 2. Illustration of Segment tree.
The segment tree for array is a binary tree in which each node contains aggregate information (min, max, sum, etc.) for a subrange of the array, as its left and right child hold information for range and .
Segment tree could be implemented using either an array or a tree. For an array implementation, if the element at index is not a leaf, its left and right child are stored at index and respectively.
In the example above (Figure 2), every leaf node contains the initial array elements {2,4,5,7,8,9}
. The internal nodes contain the sum of the corresponding elements in range - (11)
for the elements from index 0 to index 2. The root (35)
being the sum of its children (6)
;(29)
, holds the total sum of the entire array.
Segment Tree can be broken down to the three following steps:
We will use a very effective bottom-up approach to build segment tree. We already know from the above that if some node holds the sum of range, its left and right children hold the sum for range and respectively.
Therefore to find the sum of node , we need to calculate the sum of its right and left child in advance.
We begin from the leaves, initialize them with input array elements . Then we move upward to the higher level to calculate the parents' sum till we get to the root of the segment tree.
Java
int[] tree; int n; public NumArray(int[] nums) { if (nums.length > 0) { n = nums.length; tree = new int[n * 2]; buildTree(nums); } } private void buildTree(int[] nums) { for (int i = n, j = 0; i < 2 * n; i++, j++) tree[i] = nums[j]; for (int i = n - 1; i > 0; --i) tree[i] = tree[i * 2] + tree[i * 2 + 1]; }
Complexity Analysis
Time complexity :
Time complexity is , because we calculate the sum of one node during each iteration of the for loop. There are approximately nodes in a segment tree.
This could be proved in the following way: Segmented tree for array with elements has leaves (the array elements itself). The number of nodes in each level is half the number in the level below.
So if we sum the number by level we will get:
Space complexity : .
We used extra space to store the segment tree.
When we update the array at some index we need to rebuild the segment tree, because there are tree nodes which contain the sum of the modified element. Again we will use a bottom-up approach. We update the leaf node that stores . From there we will follow the path up to the root updating the value of each parent as a sum of its children values.
Java
void update(int pos, int val) { pos += n; tree[pos] = val; while (pos > 0) { int left = pos; int right = pos; if (pos % 2 == 0) { right = pos + 1; } else { left = pos - 1; } // parent is updated after child is updated tree[pos / 2] = tree[left] + tree[right]; pos /= 2; } }
Complexity Analysis
Time complexity : .
Algorithm has time complexity, because there are a few tree nodes with range that include th array element, one on each level. There are levels.
Space complexity : .
We can find range sum query using segment tree in the following way:
Algorithm hold loop invariant:
and sum of and has been calculated, where and are the left and right boundary of calculated sum. Initially we set with left leaf and with right leaf . Range shrinks on each iteration till range borders meets after approximately iterations of the algorithm
Java
public int sumRange(int l, int r) { // get leaf with value 'l' l += n; // get leaf with value 'r' r += n; int sum = 0; while (l <= r) { if ((l % 2) == 1) { sum += tree[l]; l++; } if ((r % 2) == 0) { sum += tree[r]; r--; } l /= 2; r /= 2; } return sum; }
Complexity Analysis
Time complexity :
Time complexity is because on each iteration of the algorithm we move one level up, either to the parent of the current node or to the next sibling of parent to the left or right direction till the two boundaries meet. In the worst-case scenario this happens at the root after iterations of the algorithm.
Space complexity : .
The iterative version of Segment Trees was introduced in this article. A more intuitive, recursive version of Segment Trees to solve this problem is discussed here. The concept of Lazy Propagation is also introduced there.
There is an alternative solution of the problem using Binary Indexed Tree. It is faster and simpler to code. You can find it here.
Analysis written by: @elmirap.