AlgoDaily

What Is A Segment Tree?

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

Introduction

Welcome to the realm of Segment Trees! If you've meandered through the pathways of data structures, you've likely bumped into various trees. But today, we're introducing a special kind of tree that's less talked about yet incredibly powerful: the Segment Tree.

What Exactly is a Segment Tree?

Imagine you're building the next big thing since sliced bread: an app that allows users to rate their bread slices (because, why not?). You need a quick way to query ratings within a range or update a rating. This is where a using a segment tree would shine.

At its core, a Segment Tree is a binary tree used for storing information about segments or intervals. It facilitates faster querying, like finding the sum, minimum, or maximum of these intervals. It's the kind of tool you didn't know you needed until now.

Theoretical

The Theory

Imagine a vast library, home to a plethora of books on myriad subjects. The library is organized into sections, each housing books on a particular topic. Now, you're the librarian, and you wish to quickly answer questions like: - How many books are there in a specific range of sections? - What's the thickest book between section A and section B?

Checking each book or even each section individually would be tedious. Enter the "Section Summary Card System" (or SSCS, because who doesn't love acronyms?). This system gives you a summarized view of every possible range of sections, allowing you to answer queries rapidly.

The Segment Tree is akin to this SCSS. It provides summarized information about various segments (or intervals) of an array, enabling efficient answers to range queries.

The Theoretical Dive

With the analogy in mind, let's understand the Segment Tree's core essence.

Structure: At its core, a Segment Tree is a binary tree. Each node represents an interval of an array, with the root representing the entire array. The two children of a node represent the two halves of the node's interval. This hierarchical structure ensures that we can access any sub-interval of the array in logarithmic time.

Storage: Each node in the Segment Tree stores aggregate information about its interval. This information could be the sum of elements in that interval, the maximum element, the minimum, etc. When we say "aggregate," it means that the information at a parent node can be derived from its children. For example, if nodes store sums, then the parent's value is the sum of its two children's values.

Building the Tree: The tree is constructed recursively. The process can be visualized as:

  1. The root node represents the entire array.
  2. Split the current interval into two halves.
  3. The left child represents the left half, and the right child represents the right half.
  4. Repeat the process for each child until you reach individual array elements.

Querying: To fetch information about a particular interval, we traverse the tree from the root. At each node, we decide:

  1. If the node's interval lies entirely inside the query interval, fetch the node's value.
  2. If the node's interval is outside the query interval, ignore it.
  3. If the node's interval partially overlaps the query interval, split the query and delve deeper into both children.

Updates: When an element of the array changes, the tree nodes representing intervals containing that element must be updated. This update is propagated from the leaf node (representing a single element) up to the root.

Why Segment Trees?

With the availability of simpler data structures, why venture into the realm of Segment Trees? The answer lies in their power and efficiency. Segment Trees offer a unique blend of logarithmic time complexities for both updates and queries. They are versatile and can be adapted for various range-query problems beyond just sums, such as range minimums, maximums, and even gcds.

Implementation

Implementation

Crafting Our Segment Tree

Let's roll up our sleeves and carve out this tree in Python:

class SegmentTree:
    def __init__(self, arr):
        self.n = len(arr)
        self.tree = [0] * (4 * self.n)
        self.build(arr, 0, 0, self.n-1)

    def build(self, arr, v, l, r):
        if l == r:
            self.tree[v] = arr[l]
        else:
            m = (l + r) // 2
            self.build(arr, v*2, l, m)
            self.build(arr, v*2+1, m+1, r)
            self.tree[v] = self.tree[v*2] + self.tree[v*2+1]
// javascript version
class SegmentTree {
    constructor(arr) {
        this.n = arr.length;
        this.tree = new Array(4 * this.n).fill(0);
        this.build(arr, 1, 0, this.n-1);
    }

    build(arr, v, l, r) {
        if (l === r) {
            this.tree[v] = arr[l];
        } else {
            let m = Math.floor((l + r) / 2);
            this.build(arr, v*2, l, m);
            this.build(arr, v*2+1, m+1, r);
            this.tree[v] = this.tree[v*2] + this.tree[v*2+1];
        }
    }
}
// java version
public class SegmentTree {
    private int[] tree;
    private int n;

    public SegmentTree(int[] arr) {
        this.n = arr.length;
        this.tree = new int[4 * n];
        build(arr, 1, 0, n-1);
    }

    private void build(int[] arr, int v, int l, int r) {
        if (l == r) {
            tree[v] = arr[l];
        } else {
            int m = (l + r) / 2;
            build(arr, v*2, l, m);
            build(arr, v*2+1, m+1, r);
            tree[v] = tree[v*2] + tree[v*2+1];
        }
    }
}
// c++ version
#include <vector>

class SegmentTree {
public:
    SegmentTree(std::vector<int>& arr) {
        n = arr.size();
        tree.resize(4 * n, 0);
        build(arr, 1, 0, n-1);
    }

private:
    std::vector<int> tree;
    int n;

    void build(std::vector<int>& arr, int v, int l, int r) {
        if (l == r) {
            tree[v] = arr[l];
        } else {
            int m = (l + r) / 2;
            build(arr, v*2, l, m);
            build(arr, v*2+1, m+1, r);
            tree[v] = tree[v*2] + tree[v*2+1];
        }
    }
};

You might be wondering, "What's this magic?" Let's break it down:

  • We begin by initializing our tree to four times the size of our input array.
  • The build function is where the magic happens. It recursively constructs our segment tree. If we're at a leaf node, we store the array element. Otherwise, we build the left and right subtrees and store their sum.
Understanding the Segment Tree with an Example

Consider an example array: arr = [1, 3, 5, 7, 9, 11].

Step 1: Initialization

When we instantiate our segment tree with this array, we initialize an array tree with zeros, which is four times the size of arr. This ensures that we have enough space to represent the tree. So our tree starts as:

tree = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Step 2: Building the Tree

The build function starts working on the array. Since the input does not represent a single element (i.e., l is not equal to r), we find the middle of the array and recursively build for the left and right halves.

For our array, the process would look something like:

  1. Start with the entire array: l=0, r=5 (entire array). Middle m=2.
  2. Recursively build the left half: l=0, r=2 (elements [1, 3, 5]). Here again, we'd split this into two halves: [1, 3] and [5].
  3. Continue this process until we reach individual elements.
  4. Once we've reached individual elements (leaf nodes), we begin storing values in the tree array and then propagate those values upwards.

Visually, the tree array evolves as: Step 1 (Initialization): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] Step 2 (Building for leaf nodes): [0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 0, 0, 7, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] Step 3 (Building upwards): [0, 0, 0, 0, 4, 5, 16, 11, 1, 3, 0, 0, 7, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] Step 4 (Final tree after aggregating values): [0, 36, 9, 27, 4, 5, 16, 11, 1, 3, 0, 0, 7, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Note: The above representation is a simplification. The tree's structure and values will vary based on the aggregation function used (in this case, sum).

By the end of the build process, our tree array has stored values at appropriate positions. The root of the tree (position 1 in the array) contains the aggregated value of the entire array. The left and right children of each node split this aggregated value further down into segments, and so on.

This segment tree structure allows us to efficiently query a range of values (like the sum between two indices) and update individual values.

More Manipulation

Querying Our Tree

With the tree built, it's time to extract some information from it!

# python version
def query(self, v, l, r, ql, qr):
    if ql <= l and r <= qr:
        return self.tree[v]
    if qr < l or r < ql:
        return 0
    m = (l + r) // 2
    return self.query(v*2, l, m, ql, qr) + self.query(v*2+1, m+1, r, ql, qr)
// javascript version
query(v, l, r, ql, qr) {
    if (ql <= l && r <= qr) {
        return this.tree[v];
    }
    if (qr < l || r < ql) {
        return 0;
    }
    let m = Math.floor((l + r) / 2);
    return this.query(v*2, l, m, ql, qr) + this.query(v*2+1, m+1, r, ql, qr);
}
// java version
public int query(int v, int l, int r, int ql, int qr) {
    if (ql <= l && r <= qr) {
        return tree[v];
    }
    if (qr < l || r < ql) {
        return 0;
    }
    int m = (l + r) / 2;
    return query(v*2, l, m, ql, qr) + query(v*2+1, m+1, r, ql, qr);
}
// c++ version
int query(int v, int l, int r, int ql, int qr) {
    if (ql <= l && r <= qr) {
        return tree[v];
    }
    if (qr < l || r < ql) {
        return 0;
    }
    int m = (l + r) / 2;
    return query(v*2, l, m, ql, qr) + query(v*2+1, m+1, r, ql, qr);
}

Each version of the query function captures the same logic and functionality as your original Python function but in the respective languages.

This function allows you to ask, "Hey tree, can you tell me the sum between indices ql and qr?" And the tree, being the wise entity it is, either provides an answer or divides the task among its children.

Updating: Because Change is the Only Constant

Sometimes, a bread slice gets extra crispy and its rating changes. We need to account for that.

# python version
def update(self, v, l, r, pos, val):
    if l == r:
        self.tree[v] = val
    else:
        m = (l + r) // 2
        if pos <= m:
            self.update(v*2, l, m, pos, val)
        else:
            self.update(v*2+1, m+1, r, pos, val)
        self.tree[v] = self.tree[v*2] + self.tree[v*2+1]
// javascript version
update(v, l, r, pos, val) {
    if (l === r) {
        this.tree[v] = val;
    } else {
        let m = Math.floor((l + r) / 2);
        if (pos <= m) {
            this.update(v*2, l, m, pos, val);
        } else {
            this.update(v*2+1, m+1, r, pos, val);
        }
        this.tree[v] = this.tree[v*2] + this.tree[v*2+1];
    }
}
// java version
public void update(int v, int l, int r, int pos, int val) {
    if (l == r) {
        tree[v] = val;
    } else {
        int m = (l + r) / 2;
        if (pos <= m) {
            update(v*2, l, m, pos, val);
        } else {
            update(v*2+1, m+1, r, pos, val);
        }
        tree[v] = tree[v*2] + tree[v*2+1];
    }
}
// c++ version
void update(int v, int l, int r, int pos, int val) {
    if (l == r) {
        tree[v] = val;
    } else {
        int m = (l + r) / 2;
        if (pos <= m) {
            update(v*2, l, m, pos, val);
        } else {
            update(v*2+1, m+1, r, pos, val);
        }
        tree[v] = tree[v*2] + tree[v*2+1];
    }
}

With this update method, we locate our slice, adjust its rating, and inform the tree about this delightful crispiness.

A Slice of Action

Let's witness our Segment Tree in action:

# python version
ratings = [4, 3, 5, 2, 1]
bread_tree = SegmentTree(ratings)

print(bread_tree.query(1, 0, bread_tree.n-1, 1, 3))  # Output: 10 (Because 3 + 5 + 2 = 10)
bread_tree.update(1, 0, bread_tree.n-1, 2, 4)        # The third slice's rating changed!
print(bread_tree.query(1, 0, bread_tree.n-1, 1, 3))  # Output: 9 (Because 3 + 4 + 2 = 9)
// javascript version
let ratings = [4, 3, 5, 2, 1];
let breadTree = new SegmentTree(ratings);

console.log(breadTree.query(1, 0, breadTree.n-1, 1, 3));  // Output: 10 (Because 3 + 5 + 2 = 10)
breadTree.update(1, 0, breadTree.n-1, 2, 4);  // The third slice's rating changed!
console.log(breadTree.query(1, 0, breadTree.n-1, 1, 3));  // Output: 9 (Because 3 + 4 + 2 = 9)
// java version
int[] ratings = {4, 3, 5, 2, 1};
SegmentTree breadTree = new SegmentTree(ratings);

System.out.println(breadTree.query(1, 0, breadTree.n-1, 1, 3));  // Output: 10 (Because 3 + 5 + 2 = 10)
breadTree.update(1, 0, breadTree.n-1, 2, 4);  // The third slice's rating changed!
System.out.println(breadTree.query(1, 0, breadTree.n-1, 1, 3));  // Output: 9 (Because 3 + 4 + 2 = 9)
// c++ version
std::vector<int> ratings = {4, 3, 5, 2, 1};
SegmentTree breadTree(ratings);

std::cout << breadTree.query(1, 0, breadTree.n-1, 1, 3) << std::endl;  // Output: 10 (Because 3 + 5 + 2 = 10)
breadTree.update(1, 0, breadTree.n-1, 2, 4);  // The third slice's rating changed!
std::cout << breadTree.query(1, 0, breadTree.n-1, 1, 3) << std::endl;  // Output: 9 (Because 3 + 4 + 2 = 9)

Each version captures the logic and functionality of your original Python code, but in the respective languages.

And there you have it! Your bread slices are now neatly organized in a Segment Tree.

```py
class SegmentTree:
    def __init__(self, arr):
        # Get the length of the input array
        self.n = len(arr)
        # Initialize the tree with 4 times the size of the input array
        self.tree = [0] * (4 * self.n)
        # Start building the tree from the root
        self.build(arr, 1, 0, self.n-1)

    def build(self, arr, v, l, r):
        # If the left and right boundary is the same (i.e., a leaf node)
        if l == r:
            # Assign the value from the array to the leaf node
            self.tree[v] = arr[l]
        else:
            # Find the middle of the current segment
            m = (l + r) // 2
            # Recursively build the left child
            self.build(arr, v*2, l, m)
            # Recursively build the right child
            self.build(arr, v*2+1, m+1, r)
            # Store the sum of the children at the current node
            self.tree[v] = self.tree[v*2] + self.tree[v*2+1]

    def query(self, v, l, r, ql, qr):
        # If the current segment is entirely inside the query range
        if ql <= l and r <= qr:
            # Return the value stored in the current node
            return self.tree[v]
        # If the current segment is entirely outside the query range
        if qr < l or r < ql:
            # Return a neutral value (0 for sum)
            return 0
        # Find the middle of the current segment
        m = (l + r) // 2
        # Recursively query the left and right children, then return their sum
        return self.query(v*2, l, m, ql, qr) + self.query(v*2+1, m+1, r, ql, qr)

    def update(self, v, l, r, pos, val):
        # If we've reached the leaf node corresponding to the array element
        if l == r:
            # Update the value
            self.tree[v] = val
        else:
            # Find the middle of the current segment
            m = (l + r) // 2
            # If the update position is in the left half
            if pos <= m:
                # Recursively update the left child
                self.update(v*2, l, m, pos, val)
            else:  # If the update position is in the right half
                # Recursively update the right child
                self.update(v*2+1, m+1, r, pos, val)
            # Update the current node based on children values
            self.tree[v] = self.tree[v*2] + self.tree[v*2+1]

def main():
    # Sample array of ratings
    ratings = [4, 3, 5, 2, 1]
    # Create a segment tree for the ratings
    bread_tree = SegmentTree(ratings)

    # Query the segment tree for the sum of ratings from index 1 to 3
    print(bread_tree.query(1, 0, bread_tree.n-1, 1, 3))  # Expected: 10 (Because 3 + 5 + 2 = 10)
    
    # Update the rating of the third slice
    bread_tree.update(1, 0, bread_tree.n-1, 2, 4)
    
    # Query the segment tree again for the sum of ratings from index 1 to 3
    print(bread_tree.query(1, 0, bread_tree.n-1, 1, 3))  # Expected: 9 (Because 3 + 4 + 2 = 9)

if __name__ == "__main__":
    # Run the main driver function
    main()

```
```js
class SegmentTree {
    constructor(arr) {
        this.n = arr.length;
        this.tree = new Array(4 * this.n).fill(0);
        this.build(arr, 1, 0, this.n-1);
    }

    build(arr, v, l, r) {
        if (l === r) {
            this.tree[v] = arr[l];
        } else {
            let m = Math.floor((l + r) / 2);
            this.build(arr, v*2, l, m);
            this.build(arr, v*2+1, m+1, r);
            this.tree[v] = this.tree[v*2] + this.tree[v*2+1];
        }
    }

    query(v, l, r, ql, qr) {
        if (ql <= l && r <= qr) {
            return this.tree[v];
        }
        if (qr < l || r < ql) {
            return 0;
        }
        let m = Math.floor((l + r) / 2);
        return this.query(v*2, l, m, ql, qr) + this.query(v*2+1, m+1, r, ql, qr);
    }

    update(v, l, r, pos, val) {
        if (l === r) {
            this.tree[v] = val;
        } else {
            let m = Math.floor((l + r) / 2);
            if (pos <= m) {
                this.update(v*2, l, m, pos, val);
            } else {
                this.update(v*2+1, m+1, r, pos, val);
            }
            this.tree[v] = this.tree[v*2] + this.tree[v*2+1];
        }
    }
}

function main() {
    let ratings = [4, 3, 5, 2, 1];
    let breadTree = new SegmentTree(ratings);

    console.log(breadTree.query(1, 0, breadTree.n-1, 1, 3));  // Expected: 10 (Because 3 + 5 + 2 = 10)
    breadTree.update(1, 0, breadTree.n-1, 2, 4);
    console.log(breadTree.query(1, 0, breadTree.n-1, 1, 3));  // Expected: 9 (Because 3 + 4 + 2 = 9)
}

main();
```
```java
public class Main {
    private int[] tree;
    private int n;

    Main(int[] arr) {
        this.n = arr.length;
        this.tree = new int[4 * n];
        build(arr, 1, 0, n-1);
    }

    private void build(int[] arr, int v, int l, int r) {
        if (l == r) {
            tree[v] = arr[l];
        } else {
            int m = (l + r) / 2;
            build(arr, v*2, l, m);
            build(arr, v*2+1, m+1, r);
            tree[v] = tree[v*2] + tree[v*2+1];
        }
    }

    public int query(int v, int l, int r, int ql, int qr) {
        if (ql <= l && r <= qr) {
            return tree[v];
        }
        if (qr < l || r < ql) {
            return 0;
        }
        int m = (l + r) / 2;
        return query(v*2, l, m, ql, qr) + query(v*2+1, m+1, r, ql, qr);
    }

    public void update(int v, int l, int r, int pos, int val) {
        if (l == r) {
            tree[v] = val;
        } else {
            int m = (l + r) / 2;
            if (pos <= m) {
                update(v*2, l, m, pos, val);
            } else {
                update(v*2+1, m+1, r, pos, val);
            }
            tree[v] = tree[v*2] + tree[v*2+1];
        }
    }

    public static void main(String[] args) {
        int[] ratings = {4, 3, 5, 2, 1};
        Main breadTree = new Main(ratings);

        System.out.println(breadTree.query(1, 0, breadTree.n-1, 1, 3));  // Expected: 10 (Because 3 + 5 + 2 = 10)
        breadTree.update(1, 0, breadTree.n-1, 2, 4);
        System.out.println(breadTree.query(1, 0, breadTree.n-1, 1, 3));  // Expected: 9 (Because 3 + 4 + 2 = 9)
    }
}
```
```cpp
// c++ version
#include <iostream>
#include <vector>

class SegmentTree {
private:
    std::vector<int> tree;
    int n;

public:
    SegmentTree(const std::vector<int>& arr) : n(arr.size()), tree(4 * n) {
        build(arr, 1, 0, n-1);
    }

    void build(const std::vector<int>& arr, int v, int l, int r) {
        if (l == r) {
            tree[v] = arr[l];
        } else {
            int m = (l + r) / 2;
            build(arr, v*2, l, m);
            build(arr, v*2+1, m+1, r);
            tree[v] = tree[v*2] + tree[v*2+1];
        }
    }

    int query(int v, int l, int r, int ql, int qr) {
        if (ql <= l && r <= qr) {
            return tree[v];
        }
        if (qr < l || r < ql) {
            return 0;
        }
        int m = (l + r) / 2;
        return query(v*2, l, m, ql, qr) + query(v*2+1, m+1, r, ql, qr);
    }

    void update(int v, int l, int r, int pos, int val) {
        if (l == r) {
            tree[v] = val;
        } else {
            int m = (l + r) / 2;
            if (pos <= m) {
                update(v*2, l, m, pos, val);
            } else {
                update(v*2+1, m+1, r, pos, val);
            }
            tree[v] = tree[v*2] + tree[v*2+1];
        }
    }
  
    int getN() const {
        return n;
    }
};

int main() {
    std::vector<int> ratings = {4, 3, 5, 2, 1};
    SegmentTree breadTree(ratings);

    std::cout << breadTree.query(1, 0, breadTree.getN()-1, 1, 3) << std::endl;  // Expected: 10 (Because 3 + 5 + 2 = 10)

    breadTree.update(1, 0, breadTree.getN()-1, 2, 4);  // Update the third slice's rating

    std::cout << breadTree.query(1, 0, breadTree.getN()-1, 1, 3) << std::endl;  // Expected: 9 (Because 3 + 4 + 2 = 9)

    return 0;
}

```

Conclusion

Conclusion

Segment Trees might initially seem like an intricate puzzle, but with a bit of practice and patience, they become an indispensable tool in your coding toolkit. They offer a beautiful blend of theory and practicality, making tasks involving array intervals seem like a breeze. Here's to more efficient coding and perfectly rated bread slices!

One Pager Cheat Sheet

One Pager Cheat Sheet

  • A Segment Tree is a special binary tree data structure used for storing information about segments or intervals, facilitating faster querying like finding the sum, minimum, or maximum of these intervals.
  • A Segment Tree is a binary tree that provides summarized information about various segments of an array, allowing efficient responses to range queries; each node represents an array interval, storing aggregate information about that interval, and the tree is structured such that any sub-interval can be accessed in logarithmic time. The Segment Tree is built recursively, splitting intervals into halves represented by child nodes, and allows for quick queries and updates by traversing from the root node and updating from the leaf up. The Segment Tree stands out for its power and efficiency in complex data structures, offering logarithmic time complexities for both updates and queries, and flexibility for a variety of range-query problems.
  • The SegmentTree class in Python includes an initialization function that prepares a tree of four times the size of the original array, and a build function that recursively constructs the segment tree; if at a leaf node, the function stores the array element, otherwise, it constructs left and right subtrees and stores their sum. This allows efficient querying of a range of values and updating of individual values in the segment tree.
  • The query and update functions in the Segment Tree allow you to respectively retrieve the sum of the tree node values between two indices and adjust a tree node value, exemplified here with a bread rating system where bread slice ratings can be adjusted and their sum queried for.
  • Segment Trees are an invaluable coding tool that simplify tasks involving array intervals, blending theory and practicality for more efficient coding.
Open interactive view →