spiralgo / algorithms

We voluntarily solve algorithm problems on this repo.
24 stars 7 forks source link

307. Range Sum Query - Mutable & 308. Range Sum Query 2D - Mutable #392

Closed ErdemT09 closed 3 years ago

ErdemT09 commented 3 years ago

Problem: https://leetcode.com/problems/range-sum-query-mutable/

Here, the problem we face when we try to sum ranges using brute force range summing is that we cannot use prefix sums. If the range was immutable, it would have been a well choice. But reacting to the updates with prefix sums itself would have been inefficient. There are few ways of resolving this inconsistency between partial array sums and update/sum queries:

Approach 1-Segment Trees

We could think of the range sums as a tree to some degree. The root of the tree has the value sum[0:length-1] = sum[0:mid]+sum[mid+1:length-1]. Thus, each node in this range tree has a value equal to the sum of its 2 children. For the base case in the leaves of the tree, the value would be equal to sum[i:i] = arr[i]. Above the base case, for example at the node with the range 0:1, the value would be equal to the sum of the values of the children = sum[0:0] + sum[1:1]. Thus, after calculating the values of a node's children, we can calculate the nodes value.

We can naturally implement this using custom classes and such, but that is inefficient. A more efficient implementation can be done through arrays.

From the building of the tree array, we know that left children are located at even indices and right children at odds. This has two implications for i and j:

-- For i: If i is an even number, that means that we need to add the sum of its parent, not just the individual node's. If i is an odd number, that means that the value of the left child should be excluded, we only add the right child's value. In the 1. case, we simply shift i to its parent index by dividing it by 2. In the 2. case, we add the right child's value. We then increment i. This would get us to the left node of the right sibling of the parent, whose value could be entirely add if i≤j.

-- For j: Likewise, if j is an odd number, that means that the node is a right child and the parent's range is included to be added. If j is an even number, this implies that the right child's value needs to be excluded. The procedure is symmetrical to the one done for i: In the 1. case, we shift j to the parent index by dividing by 2 (integer division). In the 2. case, we add the left child's value. We then decrement i. This would get us to the right node of the left sibling of the parent, whose value could be entirely add if i≤j.

We do these 2 procedure together.

ErdemT09 commented 3 years ago

Approach 2-Binary Indexed Tree (Fenwick Tree)

Instead of using an array covering entire half-ranges, we can have a structure that is more flexible in terms of space. Instead of having duplicate ranges as array elements, we can have the array split into small chunks to be summed: image

Building

Here, we have parent-child structure like 7->3->1->0. It is obvious that the difference between these indices are powers of two. At 0 indexing, we can efficiently apply this by:

  1. Initialize the tree sum values with array values.
  2. For each index i: 2.1. Find its parent index = i + least significant bit of (i+1), parent index of 11 = 11 + least significant bit of(12) = 11 + 4 = 15 2.2. Add the values of bit[i] to bit[parent] Since parent indices always come after their cihldren's, we accumulate their sum. When we get to the index 3, it would already have the values of 0,1(0's value came with 1) and 2 summed.

Updating

Updating is equivalent to adding the difference of the previous value of an element. We add this new value to the given index. Then, we get its parent by summing it with its least significant bit, then add the new value to its parent's sum. We repeat this until the parent is out of bounds of the array.,

Summing

Something to be noted is that getting range sums is equivalent to subtracting one prefix sum from another. In this case, we can get to the previous sum range by subtracting the least significant bit. To get the prefix sum of 5, we first directly get its sum. To move back to the previous range, we subtract its successor's least significant bit from it to get to 3. We add 3 and repeat. After adding 3, subtracting the bit would result in a negative number, meaning that there are no ranges before. Same happens with 7 too, trying to subtract 8 from it results in -1. After getting the two prefix sums this way, we return their difference.

Note: In implementation, we get the least significant bit of the successor of a number because of 0 indexing,. In 1-indexing, we would directly get the least significant bit of the number.

ErdemT09 commented 3 years ago

Approach 3-Half Counting Solution

When summing a range, if the length of the range is larger than half of the array's, then we can subtract values out of the range from the sum of the whole array instead of counting the range itself. This seems pretty slow in theory. However, it is fast fast in implementation.

There is also the solution trying to split the array into chunks of square root size, but I'm leaving it aside.

ErdemT09 commented 3 years ago

304. Range Sum Query 2D - Immutable

Problem: https://leetcode.com/problems/range-sum-query-2d-immutable/

Here, we use the same matrix prefix calculation algorithm we used for #358. It's pretty simple.

ErdemT09 commented 3 years ago

308. Range Sum Query 2D - Mutable

Resolves: #236

Approach 1-2D Segment Tree

As a 1D segment tree, stores the sum of index ranges that can be treated like column ranges, with an extra dimension, we can store sums for row ranges. Thus, we can use a segment tree of segment trees for this problem. For a matrix of size 4X4, we can have an array as a root at the top that stores the column sum ranges for all the rows [0:3]. It's children store range sums for [0:1] and [2:3]. At the bottom, we have individual segment trees for the rows 0, 1, 2, 3. Implementing this with the logic from 1D segment tree is pretty simple too. After setting up the base with the original matrix values, we move up to store sums of single indexes of row ranges to the end of the array, we calculate [2:3][1:1] for example. Then, for every single segment tree, we get their range sums, so the roots.

Update and Sum Region operations also make use of this segment tree of segment trees structure.

Approach 2-2D Binary Indexed Tree

I think efficiently implementing BIT in 2D is harder. The implementation here can actually be more efficient, when we treat as we have treated the implementation of 2D Segment Tree. It's nevertheless actually faster than the segment tree. Here, we basically do all the operations we had done in 1D in 2D. For parent finding, we consider 2 dimensions. For sibling finding, that's also the case. Finally, for the prefix sums, we calculate prefix rectangle sums.

Approach 3-Column Sums

Unlike the previous two, this approach takes linear time in theory, but it's quite fast in actual usage because of low input constraints. For each column, we can take its column sum at sum row. Updating is equivalent to adding the difference of the previous value to all elements in the column below the given row value. Summing a region is equivalent to summing the difference of the column sums of two rows over the given column range.

altay9 commented 3 years ago

The essence of all these solutions is "modularization" (and our container idea) indeed.

We divide the array into multiple segments to prevent the states of the other segments being affected just because an independent part is updated.

ErdemT09 commented 3 years ago

Further notes for Segment and Fenwick Trees: We can also add other usage of these trees. Namely, min/max finding, greatest common divisor finding, range/region updates, zero counting etc.

More resources: https://cp-algorithms.com/data_structures/segment_tree.html https://leetcode.com/articles/a-recursive-approach-to-segment-trees-range-sum-queries-lazy-propagation/ https://cp-algorithms.com/data_structures/fenwick.html https://www.topcoder.com/thrive/articles/Binary%20Indexed%20Trees https://github.com/williamfiset/Algorithms/tree/master/src/main/java/com/williamfiset/algorithms/datastructures/fenwicktree