๐ Segment Tree
This page provides links to solutions for problems that use the Fenwick Tree data structure.
Overviewโ
A Segment tree is a type of data structure that allows range operations, such as finding total, minimum, maximum, distinct value within a range, in time complexity.
Additionally, it can perform point and range updates in time as well. Despite its name, a segment tree is actually stored as an array of elements.
Build Segment Treeโ
Let's build a range sum segment tree for following array.
Even thought it is called a segment tree, it is stored as an array. To build a segment tree for an input array with elements, we start by creating an array of size times the size of an input array.
Since there are elements in an input array, build function will start with range . Segment tree array index will be at .
Build function splits the range in half, the left segment range becomes . Segment tree array index when it goes to left subtree becomes .
Recursively splitting the left subtree further, we get the left segment range as . Segment tree array index becomes .
Further splitting the left subtree, the segment reduces to a single point . At this point, we assign the element from the input array at index to current segment tree array index, so we assign tree[] = arr[].
After reaching the segment, we cannot split the left subtree further. The build function backtracks and splits the right subtree of the parent into two halves, resulting in a single point segment .
Since this is a single point, we assign the value from the input array at index to the current segment tree array index. Therefore, we assign tree[] = arr[].
At this point, we have completed splitting both the left and right subtree of the segment range . We then apply the sum function to combine the values from both child indexes and assign the result to the current segment tree array index. Thus we assign tree[] = tree[] + tree[] = .
After completing the recursive process described above, the segment tree will be fully built. Below are snapshots of each step.
- Java
long[] arr;
long[] tree;
public SegmentTreeSum(long[] arr) {
this.arr = arr;
this.tree = new long[4 * arr.length];
build(0, 0, arr.length - 1);
}
public void build(int treeIndex, int fromArrIndex, int toArrIndex) {
if (fromArrIndex == toArrIndex) {
tree[treeIndex] = arr[fromArrIndex];
}
else {
int mid = fromArrIndex + (toArrIndex - fromArrIndex) / 2;
build(2 * treeIndex + 1, fromArrIndex, mid);
build(2 * treeIndex + 2, mid + 1, toArrIndex);
tree[treeIndex] = tree[2 * treeIndex + 1] + tree[2 * treeIndex + 2];
}
}
Use Segment Treeโ
Let's use the segment tree to find the range sum of indexes in an input array.
We begin calculation of the range sum from the root node, which represent index of a segment tree array and range of an input array.
There are main conditions to consider at any given node:
-
Outside of Input Range: Return if the range at the current node falls entirely outside the input range.
-
Subset of Input Range: Return the precomputed sum from the segment tree if the range at the current node is part of input range.
-
Partially Overlaps Input Range: If range at current node partially overlaps with input range recursively calculate values from left and right subtree and return the result.
The range at current node, , partially overlaps input range , therefore we recursively calculate value from left and right subtree.
Let's recursively calculate the value from the left subtree. The range at the current node, , partially overlaps with the input range , therefore we recursively calculate the values from its left and right subtrees.
The next node in the path has a range of , which lies entirely outside the input range , therefore we return from the left subtree and stop the recursion for the left subtree.
Let's continue the recursive process on the right subtree.
The next node in the right subtree has a range of , which is part of input range . As a result, we return the precomputed value from the right subtree and stop the recursion for the right subtree.
At this stage, the values from both subtrees at the node with the range are ready. We add these values together and backtrack to the parent node.
Let's continue the above process, until we calculate the sum for the range .
The node's range partially overlaps the input range.
The node's range is subset of the input range.
The node's range is outside the input range.
Backtracking to get sum for input range.
Therefore, the range sum from index is .
- Java
public int rangeSum(int treeIndex, int from, int to) {
return rangeSumInternal(treeIndex, from, to, 0, arr.length - 1);
}
public int rangeSumInternal(int treeIndex, int fromArrIndexInput, int toArrIndexInput,
int fromArrIndex, int toArrIndex) {
// range at current node is part of input range.
// fromArrIndexInput <= fromArrIndex <= toArrIndex <= toArrIndexInput
if (fromArrIndexInput <= fromArrIndex && toArrIndex <= toArrIndexInput) {
return tree[treeIndex];
}
// range at current node is out side the input range.
// case 1: fromArrIndex < toArrIndex < fromArrIndexInput < toArrIndexInput
// case 2: fromArrIndexInput < toArrIndexInput < fromArrIndex < toArrIndex
else if (toArrIndex < fromArrIndexInput || toArrIndexInput < fromArrIndex) {
return 0;
}
// partial overlap
else {
int mid = fromArrIndex + (toArrayIndex - fromArrIndex) / 2;
// calculate sum from left subtree.
int left = rangeSumInternal(
2 * treeIndex + 1,
fromArrIndexInput, toArrIndexInput,
fromArrIndex, mid
);
// calculate sum from right subtree.
int right = rangeSumInternal(
2 * treeIndex + 2,
fromArrIndexInput, toArrIndexInput,
mid + 1, toArrIndex
);
// sum left and right subtree values together and return.
return left + right;
}
}
Update Segment Treeโ
There are two ways to update segment tree as below:
Point Updateโ
Let's update the element at index of an input array from to .
Note that the update here replaces the old value with the new value, rather than adding the new value to the existing one.
We begin the segment tree update from the root node, which represents the index of the segment tree array and the range of the input array.
There are main conditions to consider at any given node:
-
Outside of Update Point: No updates are required if the range at the current node falls entirely outside the update point.
-
Equals the Update Point: If range at current node range equals the update point, update the value at the current node.
-
Partially Overlaps Update Point: If the range at the current node partially overlaps with the update point, recursively update the subtree in which the point is present.
The range at the current node, , partially overlaps with the update point . During the segment tree construction, ranges are split into halves. The left subtree covers the range , and the right subtree covers the range . Since the update point falls within the range of the right subtree, we proceed to recursively update the right subtree.
The range of the current node is , and the update point still lies within the right subtree.
The range at the current node is , which matches the update point. We update the value at this node to and then backtrack to update the parent nodes until we return to the root.
While backtracking, we retrieve the old value of from the left subtree and the newly assigned value of from the right subtree. We then update the node with the range to the new value of .
Upon further backtracking, we reach the root node, where we retrieve the old value of from the left subtree and the new value of from the right subtree. We then update the root node to the new value of .
Once the root node update process is complete, we obtain the new segment tree as shown below.
- Java
public void pointUpdate(int arrIndex, int value) {
return pointUpdateInternal(0, arrIndex, value, 0, arr.length - 1);
}
public void pointUpdateInternal(int treeIndex, int arrIndexInput, int value,
int fromArrIndex, int toArrIndex) {
// out of the input range.
// case 1: arrIndexInput < fromArrIndex < toArrIndex
// case 2: fromArrIndex < toArrIndex < arrIndexInput
if (arrIndexInput < fromArrIndex || toArrIndex < arrIndexInput) {
return;
}
// if an index is found.
else if (fromArrIndex == toArrIndex) {
tree[treeIndex] = value;
}
// partial overlap
else {
int mid = fromArrIndex + (toArrIndex - fromArrIndex) / 2;
// check if update point is on the left subtree.
if (arrIndexInput < mid) {
pointUpdateInternal(2 * treeIndex + 1,
arrIndexInput, value
fromArrIndex, mid
);
}
// else update point is on the right subtree.
else {
pointUpdateInternal(2 * treeIndex + 2,
arrIndexInput, value,
mid + 1, toArrIndex
);
}
tree[treeIndex] = tree[2 * treeIndex + 1] + tree[2 * treeIndex + 2];
}
}
Range Updateโ
Let's update the elements in original within the range to .
Note that with range updates, the update function cannot directly modify the actual array, as doing so may require time, where is total number of elements in an array. Instead, the update function will only modify the segment tree array only.
We begin the segment tree update from the root node, which represents the index of the segment tree array and the range of the input array.
There are main conditions to consider at any given node:
-
Outside of Input Range: No updates are required if the range at the current node falls entirely outside the input range.
-
Subset of Input Range:
- Single Point: If the range at the current node reduces to a single point, update the value at current node in segment tree.
- Range Overlap: If the range at the current node is subset of update range, recursively update both subtrees.
-
Partially Overlaps Input Range: If the range at the current node partially overlaps with the input range recursively update both subtrees.
The only difference between a range update and a point update lies in point : in a range update, both subtrees are recursively updated, whereas in a point update, only the subtree containing the update point is updated.
The range of the current node, , partially overlaps with the update range, , so we update both subtrees, recursively.
Let's update the left subtree first. The range at the current node, , falls completely outside the update range, , so we return and proceed with the right subtree of parent node.
The range of the current node, , completely overlaps with the update range, , so we recursively update both subtrees.
Let's update the left subtree first. The range at the current node, , has been reduced to a single point, so we update the value of the current node to the update value, .
Let's also update the right subtree. The range at the current node, , has been reduced to a single point, so we update the value of the current node to the update value, .
At this point, the function will backtrack, and once it reaches the root node, the update will be complete. During backtracking, it will first reach the node with the range and update its value using the new values from the left and right subtrees.
On further backtracking, the update function will reach the root node, which will pull the old value of from the left subtree and the new value of from the right subtree and assign a value of to current node.
Once the root node update process is complete, we obtain the new segment tree as shown below.
- Java
public void rangeUpdate(int from, int to, int value) {
return rangeUpdateInternal(0, from, to, value, 0, arr.length - 1);
}
public void rangeUpdateInternal(int treeIndex, int fromArrIndexInput,
int toArrIndexInput, int value, int fromArrIndex, int toArrIndex) {
// outside of the input range
// case 1: fromArrIndexInput < toArrIndexInput < fromArrIndex < toArrIndex
// case 2: fromArrIndex < toArrIndex < fromArrIndexInput < toArrIndexInput
if (toArrIndexInput < fromArrIndex || toArrIndex < fromArrIndexInput) {
return;
}
// input range reduces to single point
else if (fromArrIndex == toArrIndex) {
tree[treeIndex] = value;
}
// partial overlap or complete overlap
else {
int mid = fromArrIndex + (toArrIndex - fromArrIndex) / 2;
rangeUpdateInternal(2 * treeIndex + 1,
fromArrIndexInput, toArrIndexInput, value,
fromArrIndex, mid
);
rangeUpdateInternal(2 * treeIndex + 2,
fromArrIndexInput, toArrIndexInput, value,
mid + 1, toArrIndex
);
tree[treeIndex] = tree[2 * treeIndex + 1] + tree[2 * treeIndex + 2];
}
}
Lazy Propagationโ
A range update in a segment tree can take time, where is the number of elements in the array. To reduce the update time to , you can use a technique called lazy propagation.
Lazy Range Updateโ
Let's update the elements in original within the range to .
We begin the segment tree update from the root node, which represents the index of the segment tree array and the range of the input array.
There are main conditions to consider at any given node:
-
Outside Input Range: No updates are required if the range at the current node falls entirely outside the input range.
-
Subset of Input Range: If the range at the current node is part of the input range, update the value of the current node to the input update value multiplied by the number of nodes in both subtrees of the current node. Also, lazily update the left and right subtrees using the lazy array.
-
Partially Overlaps Input Range: If the range at the current node partially overlaps with the input range, recursively update both subtrees.
The range of the current node, , partially overlaps with the update range, , so we update both subtrees, recursively.
Notice how we are using another array, called lazy, to delay updates to the left and right subtrees when the current node's range matches the input range.
Let's recursively update left subtree first. The range of the current node, , falls completely outside the update range, , so we return from the update function without taking any action.
Let's update the right subtree now. The range of the current node, , is a subset of the update range, . Therefore, we update the value of the current node to . This value is calculated as the update value() multiplied by the number of nodes in the left and right subtrees of the current node: .
To calculate the number of nodes in the current node's left and right subtrees, we simply subtract the lower limit, , from the upper limit, , of the current node and add one.
Additionally, we lazily update the left and right subtrees of the current node with the value , by modifying the lazy array.
At this point, the lazy range update function begins to backtrack. The first node it reaches is the root node. The root node retrieves the old value of from the left subtree and the new value of from the right subtree, then assigns itself the sum .
Once the root node is updated, we get segment tree as below.
- Java
public void rangeUpdateLazy(int from, int to, int value) {
return rangeUpdateLazyInternal(0, from, to, value, 0, arr.length - 1);
}
public void rangeUpdateLazyInternal(int treeIndex, int fromArrIndexInput,
int toArrIndexInput, int value, int fromArrIndex, int toArrIndex) {
// propagate old lazy value from the current node to left and righ subtree
if (lazy[treeIndex] != 0) {
int totalLazyValue = lazy[treeIndex] * (toArrIndex - fromArrIndex + 1);
tree[treeIndex] = totalLazyValue;
if(fromArrIndex != toArrIndex) {
lazy[2 * treeIndex + 1] = lazy[treeIndex];
lazy[2 * treeIndex + 2] = lazy[treeIndex];
}
lazy[treeIndex] = 0
}
// outside of the input range
// case 1: fromArrIndexInput < toArrIndexInput < fromArrIndex < toArrIndex
// case 2: fromArrIndex < toArrIndex < fromArrIndexInput < toArrIndexInput
if (toArrIndexInput < fromArrIndex || toArrIndex < fromArrIndexInput) {
return;
}
// subset of input range
// fromArrIndexInput <= fromArrIndex <= toArrIndex <= toArrInputIndex
else if (fromArrIndexInput <= fromArrIndex && toArrIndex <= toArrInputIndex) {
int totalUpdateValue = value * (toArrIndex - fromArrIndex + 1);
tree[treeIndex] = totalUpdateValue;
if(fromArrIndex != toArrIndex) {
// delay propagation of current updates to child nodes
lazy[2 * treeIndex + 1] = value;
lazy[2 * treeIndex + 2] = value;
}
lazy[treeIndex] = 0;
}
// partial overlap
else {
int mid = fromArrIndex + (toArrIndex - fromArrIndex) / 2;
rangeUpdateLazyInternal(2 * treeIndex + 1,
fromArrIndexInput, toArrIndexInput, value,
fromArrIndex, mid
);
rangeUpdateLazyInternal(2 * treeIndex + 2,
fromArrIndexInput, toArrIndexInput, value,
mid + 1, toArrIndex
);
tree[treeIndex] = tree[2 * treeIndex + 1] + tree[2 * treeIndex + 2]
}
}
Lazy Range Queryโ
Let's use the segment tree to find the range sum of indexes in an input array using lazy range query.
There are main conditions to consider at any given node:
-
Outside of Input Range: Return if the range at the current node falls entirely outside the input range.
-
Subset of Input Range: If the range at the current node is part of input range and if lazy value is zero, return the precomputed sum from the segment tree.
-
Partially Overlaps Input Range: If range at current node partially overlaps with input range recursively calculate values from left and right subtree and return the result.
In a lazy range query, we also propagate and retrieve values from the lazy array.
The range at current node, , partially overlaps input range , therefore we recursively calculate value from left and right subtree.
Let's first recursively calculate the sum from the left subtree. The range at current node, , partially overlaps input range , therefore we recursively calculate value from left and right subtree.
Continuing further into the left subtree, the range of the current node, , is completely outside the input range, . Therefore, we return .
Let's recursively calculate the sum from the right subtree of parent node. The range at the current node, is subset of input range , so we return precomputed value from the current node.
At this point, the update function backtracks and retrieves from the left subtree and from the right subtree.
Let's recursively calculate the sum from the right subtree of the root node. The range of the current node, , partially overlaps with the input range, . Therefore, we calculate the sum from both the left and right subtrees.
Let's calculate the sum from the left subtree first. The range of the current node, , is a subset of the input range, . Therefore, we return the precomputed value. However, since the node has a lazy value, it should be propagated first, and the new value for the node should be recalculated.
Let's calculate the sum from right subtree. The range of the current node, , falls outside the input range . Thereforce, we return from the right subtree.
At this point lazy range update function backtracks and calculates sum value over range as .
- Java
public int rangeSumLazy(int from, int to) {
return rangeSumLazyInternal(0, from, to, 0, arr.length - 1);
}
public int rangeSumLazyInternal(int treeIndex, int fromArrIndexInput,
int toArrIndexInput, int fromArrIndex, int toArrIndex) {
// propagate old lazy value
if (lazy[treeIndex] != 0) {
int totalLazyValue = lazy[treeIndex] * (toArrIndex - fromArrIndex + 1)
tree[treeIndex] = tree[treeIndex] + totalLazyValue
if(fromArrIndex != toArrIndex) {
lazy[2 * treeIndex + 1] = lazy[treeIndex]
lazy[2 * treeIndex + 2] = lazy[treeIndex]
}
lazy[treeIndex] = 0
}
// outside of input range
// fromArrIndexInput < toArrIndexInput < fromArrIndex < toArrIndex
// fromArrIndex < toArrIndex < fromArrIndexInput < toArrIndexInput
if (toArrIndexInput < fromArrIndex || toArrIndex < fromArrIndexInput) {
return 0;
}
// subset of input range
// fromArrIndex <= fromArrIndexInput <= toArrInputIndex <= toArrIndex
else if (fromArrIndexInput <= fromArrIndex && toArrIndex <= && toArrInputIndex) {
return tree[treeIndex]
}
// partial overlap
else {
int mid = fromArrIndex + (toArrIndex - fromArrIndex) / 2;
int left = rangeSumLazyInternal(2 * treeIndex + 1,
fromArrIndexInput, toArrIndexInput,
fromArrIndex, mid
)
int right = rangeSumLazyInternal(2 * treeIndex + 2,
fromArrIndexInput, toArrIndexInput,
mid + 1, toArrIndex
)
return left + right;
}
}
Complexityโ
Let's say there are elements in an array.
Time Complexityโ
Time complexity for building segment tree is:
Time complexity for point update, range update and range sum is:
Space Complexityโ
Segment tree uses two seperate array for storing segment tree and lazy updates, so space complexity is:
How to Spot These Problemsโ
You can identify segment tree problems if the problem requires you to:
- Quickly calculate the sum of elements from the start of an array up to a certain index.