307. Range Sum Query - Mutable


Given an integer array nums, find the sum of the elements between indices i and j (ij), 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:

  1. The array is only modifiable by the update function.
  2. You may assume the number of calls to update and sumRange function is distributed evenly.


Summary

This article is for intermediate level readers. It introduces the following concepts: Range sum query, Sqrt decomposition, Segment tree.

Solution

Approach #1 (Naive) [Time Limit Exceeded]

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 : .

Approach #2 (Sqrt decomposition) [Accepted]

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

Range sum query using SQRT decomposition

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.

Approach #3 (Segment tree) [Accepted]

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.

Illustration of Segment tree

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:

  1. Pre-processing step which builds the segment tree from a given array.
  2. Update the segment tree when an element is modified.
  3. Calculate the Range Sum Query using the segment tree.
1. Build segment tree

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.

2. Update 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 : .

3. Range Sum Query

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

  • Loop till
    • Check if is right child of its parent
      • is right child of . Then contains sum of range of and another child which is outside the range and we don't need parent sum. Add to without its parent and set to point to the right of on the upper level.
      • is not right child of . Then parent contains sum of range which lies in . Add to and set to point to the parent of
    • Check if is left child of its parent
      • is left child of . Then contains sum of range of and another child which is outside the range and we don't need parent sum. Add to without its parent and set to point to the left of on the upper level.
      • is not left child of . Then parent contains sum of range which lies in . Add to and set to point to the parent of

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 : .

Further Thoughts

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.