Closed ErdemT09 closed 3 years ago
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:
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:
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.,
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.
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.
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.
Resolves: #236
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.
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.
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.
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.
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
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 tosum[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.
Build
Let's say that the root of the tree is located at the index 1. Then we can create a level-order tree where the next 2 indices are the children of the root at index 1, i.e. 2=left child, 3=right child. From this, we can infer that the second half of the tree array will consist of the base case values of individual indices. After defining these values, to continue building up in a bottom-up manner, we start at the index before the half, and the values as
tree[i] = tree[2*i] + tree[2*+1], 2*i = left child, 2*i+1 right child
. Fortree[]
, we naturally need an array of size2*n
(n=array length).Range Sum Queries
Let
i
be the left index of the query,j
the right. Initially, we must addn
to both i and j because their base cases are locatedn
indices after their original array locations.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 ifi≤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 fori
: 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 ifi≤j
.We do these 2 procedure together.
Update Queries
This is pretty simple too. We first set tree[i+n] to the value, then update its parents values: tree[i/2] = tree[i] + tree[i^1]. The xor operation is a shortcut for using the left child if i is the right, or the right child if i is the left.