Thunder8971's blog

By Thunder8971, 12 months ago, In English

Building Segment Trees Iteratively: A Step-by-Step Guide

This blog aims to explain a simple iterative segment tree that is easy to memorize due to its step-by-step approach while remaining efficient. This segment tree is designed for competitive programming contests that do not allow templates but still require this technique to be implemented quickly.

Introduction

The segment tree is a data structure that enables range queries with updates. In this blog, I will demonstrate how to construct an iterative segment tree with this approach to solve the following operations:

  1. Query the minimum value in a range.
  2. Add a given value to each element in a range.

Step 1: Preparations

To begin, we need to adjust the size of the given array. For instance, if the array is named $$$A$$$ and has a size $$$N$$$, we must find the smallest power of $$$2$$$ greater than or equal to $$$N$$$. This adjustment is possible because the additional values we append to $$$A$$$ will never be accessed. To achieve this, we first implement the function $$$\text{getPower(N)}$$$.

// Function to find the smallest power of 2 greater than or equal to n
int getPower(int n) {
    int l = 0, r = 26; // Define the binary search range for powers of 2
    while (l < r) {
        int mid = (l + r) / 2; // Calculate the midpoint of the range
        if ((1 << mid) >= n) { // Check if 2^mid is greater than or equal to n
            r = mid; // Narrow the search range to the left
        } else {
            l = mid + 1; // Narrow the search range to the right
        }
    }
    return l; // Return the smallest power of 2
}

At this point, $$$N$$$ is now a power of $$$2$$$, so it is essential to keep this detail in mind moving forward.

Step 2: Creating the Segment Tree

As we know, a Segment Tree is a binary tree that, when the original array (of size $$$N$$$) has a size that is a power of $$$2$$$, uses $$$2N$$$ nodes. Since $$$N$$$ is a power of $$$2$$$, the indices corresponding to $$$A$$$ in the Segment Tree will be $$$i+n-1$$$. The construction takes advantage of the fact that if we have a node with index $$$x$$$, then its parent is $$$\frac{x}{2}$$$. To build the Segment Tree, we first assign the values of the array to the leaves, and then for each node, we combine the results of its children to form each node. The children of each node are $$$2*x$$$ and $$$2x+1$$$.

vector<int> st(2 * n + 1), lz(2 * n + 1), A(n + 1);
// Function to build the segment tree
void build() {
    // Assign values from the array to the leaves of the segment tree
    for (int i = 1; i <= n; i++) {
        A[i] = st[i + n - 1]; // Map array elements to the corresponding leaf nodes
    }
    // Combine child nodes to form parent nodes
    for (int i = n - 1; i >= 1; i--) {
        st[i] = min(st[2 * i], st[2 * i + 1]); // Compute the minimum of the left and right children
    }
}

With this, the segment tree is ready to start performing queries and modifications.

Step 3: Creating the Query Function

To solve the problems, we need to view everything from the leaves to the root. Suppose we want a query in the interval $$$[a,b]$$$. Since we know that $$$N$$$ is a power of $$$2$$$, the node for $$$a$$$ will be $$$a+N - 1$$$ and similarly for $$$b$$$. However, this is not enough to solve the problem, as we need to filter which nodes are the main ones for our interval and which are not. To do this, we define $$$L$$$ and $$$R$$$ as the left and right limits, respectively. If the left limit is at a left node, then that node is not a definitive node in the interval because, as $$$R$$$ is the right limit, the node $$$L+1$$$ is also part of the interval, or if $$$L=R$$$, then either $$$L$$$ is on the right and necessary, or $$$R$$$ is on the left and necessary. The same applies to $$$R$$$, but in reverse.

// Function to query the minimum value in a range [a, b]
int query(int a, int b) {
    int l = a + n - 1, r = b + n - 1; // Convert range to segment tree indices
    bool reg = false; // Flag to check if the result is initialized
    int ans = 0; // Variable to store the result
    while (l <= r) {
        if (l % 2 == 1) { // If l is a right child
            propagateUpToX(l); // Propagate updates to the current node
            ans = reg ? min(ans, st[l]) : st[l]; // Update the result with the minimum value
            reg = true; // Mark the result as initialized
            l++; // Move to the next node
        }
        if (r % 2 == 0) { // If r is a left child
            propagateUpToX(r); // Propagate updates to the current node
            ans = reg ? min(ans, st[r]) : st[r]; // Update the result with the minimum value
            reg = true; // Mark the result as initialized
            r--; // Move to the previous node
        }
        l /= 2; // Move to the parent node
        r /= 2; // Move to the parent node
    }
    return ans; // Return the minimum value in the range
}

At this point, we have a function that solves the minimum query, so now let's see how to add range updates with lazy propagation.

Step 4: Adding Lazy Propagation

Now that we have finished the first point, we need to start with the next point. To address this problem, we will use the lazy propagation technique, which involves placing an update tag and updating only when necessary. Usually, this technique is implemented from the root node to the leaves (note that this is the reverse of our iterative segment tree), but we can take advantage of something to make the same traversal, and that is the binary representation of numbers, from the highest power to the lowest. If that power is present, we move to the right; otherwise, we move to the left. As we progress through the nodes to be updated, the update tags are propagated. The main update function goes down to the leaves, captures the main nodes, and calls the rest of the functions we have defined.

// Function to propagate lazy updates to a node
void propagate(int x) {
    st[x] += lz[x]; // Apply the lazy value to the current node
    if (x < n) { // If the node is not a leaf
        lz[2 * x] += lz[x]; // Propagate the lazy value to the left child
        lz[2 * x + 1] += lz[x]; // Propagate the lazy value to the right child
    }
    lz[x] = 0; // Clear the lazy value for the current node
}
// Function to propagate updates from the root to a specific node x
void propagateUpToX(int x) {
    int cur = x, h = 0; // Start from the target node and calculate its height
    while (cur > 0) {
        h++; // Increment the height
        cur /= 2; // Move to the parent node
    }
    cur = 1; // Start from the root
    for (int i = h - 2; i >= 0; i--) {
        propagate(cur); // Propagate updates to the current node
        cur = (x & (1 << i)) ? 2 * cur + 1 : 2 * cur; // Move to the left or right child based on the bit
    }
    propagate(cur); // Propagate updates to the target node
}
// Function to update the segment tree up to a specific node x
void updateUpToX(int x) {
    int cur = x; // Start from the target node
    propagate(cur); // Propagate updates to the current node
    cur /= 2; // Move to the parent node
    while (cur > 0) {
        propagate(cur); // Propagate updates to the current node
        propagate(2 * cur); // Propagate updates to the left child
        propagate(2 * cur + 1); // Propagate updates to the right child
        st[cur] = min(st[2 * cur], st[2 * cur + 1]); // Update the current node with the minimum value of its children
        cur /= 2; // Move to the parent node
    }
}
// Function to update a range [a, b] with a value v
void update(int a, int b, int v) {
    int l = a + n - 1, r = b + n - 1; // Convert range to segment tree indices
    while (l <= r) {
        if (l % 2 == 1) { // If l is a right child
            propagateUpToX(l); // Propagate updates to the current node
            lz[l] += v; // Add the value to the lazy array
            updateUpToX(l); // Update the segment tree up to the current node
            l++; // Move to the next node
        }
        if (r % 2 == 0) { // If r is a left child
            propagateUpToX(r); // Propagate updates to the current node
            lz[r] += v; // Add the value to the lazy array
            updateUpToX(r); // Update the segment tree up to the current node
            r--; // Move to the previous node
        }
        l /= 2; // Move to the parent node
        r /= 2; // Move to the parent node
    }
}

With this, we have range updates, but one thing is missing. The query section also requires updating the nodes up to the nodes we need, and for that, we just need to add a few lines to the code.

// Function to query the minimum value in a range [a, b]
int query(int a, int b) {
    int l = a + n - 1, r = b + n - 1; // Convert range to segment tree indices
    bool reg = false; // Flag to check if the result is initialized
    int ans = 0; // Variable to store the result
    while (l <= r) {
        if (l % 2 == 1) { // If l is a right child
            propagateUpToX(l); // Propagate updates to the current node
            ans = reg ? min(ans, st[l]) : st[l]; // Update the result with the minimum value
            reg = true; // Mark the result as initialized
            l++; // Move to the next node
        }
        if (r % 2 == 0) { // If r is a left child
            propagateUpToX(r); // Propagate updates to the current node
            ans = reg ? min(ans, st[r]) : st[r]; // Update the result with the minimum value
            reg = true; // Mark the result as initialized
            r--; // Move to the previous node
        }
        l /= 2; // Move to the parent node
        r /= 2; // Move to the parent node
    }
    return ans; // Return the minimum value in the range
}

All the code together would be:

// Organized and cleaned-up code for better readability

// Global variables
vector<int> st(2 * n + 1), lz(2 * n + 1), A(n + 1);

// Function to find the smallest power of 2 greater than or equal to n
int getPower(int n) {
    int l = 0, r = 26; // Define the binary search range for powers of 2
    while (l < r) {
        int mid = (l + r) / 2; // Calculate the midpoint of the range
        if ((1 << mid) >= n) { // Check if 2^mid is greater than or equal to n
            r = mid; // Narrow the search range to the left
        } else {
            l = mid + 1; // Narrow the search range to the right
        }
    }
    return l; // Return the smallest power of 2
}

// Function to build the segment tree
void build() {
    // Assign values from the array to the leaves of the segment tree
    for (int i = 1; i <= n; i++) {
        A[i] = st[i + n - 1]; // Map array elements to the corresponding leaf nodes
    }
    // Combine child nodes to form parent nodes
    for (int i = n - 1; i >= 1; i--) {
        st[i] = min(st[2 * i], st[2 * i + 1]); // Compute the minimum of the left and right children
    }
}

// Function to propagate lazy updates to a node
void propagate(int x) {
    st[x] += lz[x]; // Apply the lazy value to the current node
    if (x < n) { // If the node is not a leaf
        lz[2 * x] += lz[x]; // Propagate the lazy value to the left child
        lz[2 * x + 1] += lz[x]; // Propagate the lazy value to the right child
    }
    lz[x] = 0; // Clear the lazy value for the current node
}

// Function to propagate updates from the root to a specific node x
void propagateUpToX(int x) {
    int cur = x, h = 0; // Start from the target node and calculate its height
    while (cur > 0) {
        h++; // Increment the height
        cur /= 2; // Move to the parent node
    }
    cur = 1; // Start from the root
    for (int i = h - 2; i >= 0; i--) {
        propagate(cur); // Propagate updates to the current node
        cur = (x & (1 << i)) ? 2 * cur + 1 : 2 * cur; // Move to the left or right child based on the bit
    }
    propagate(cur); // Propagate updates to the target node
}

// Function to update the segment tree up to a specific node x
void updateUpToX(int x) {
    int cur = x; // Start from the target node
    propagate(cur); // Propagate updates to the current node
    cur /= 2; // Move to the parent node
    while (cur > 0) {
        propagate(cur); // Propagate updates to the current node
        propagate(2 * cur); // Propagate updates to the left child
        propagate(2 * cur + 1); // Propagate updates to the right child
        st[cur] = min(st[2 * cur], st[2 * cur + 1]); // Update the current node with the minimum value of its children
        cur /= 2; // Move to the parent node
    }
}

// Function to update a range [a, b] with a value v
void update(int a, int b, int v) {
    int l = a + n - 1, r = b + n - 1; // Convert range to segment tree indices
    while (l <= r) {
        if (l % 2 == 1) { // If l is a right child
            propagateUpToX(l); // Propagate updates to the current node
            lz[l] += v; // Add the value to the lazy array
            updateUpToX(l); // Update the segment tree up to the current node
            l++; // Move to the next node
        }
        if (r % 2 == 0) { // If r is a left child
            propagateUpToX(r); // Propagate updates to the current node
            lz[r] += v; // Add the value to the lazy array
            updateUpToX(r); // Update the segment tree up to the current node
            r--; // Move to the previous node
        }
        l /= 2; // Move to the parent node
        r /= 2; // Move to the parent node
    }
}

// Function to query the minimum value in a range [a, b]
int query(int a, int b) {
    int l = a + n - 1, r = b + n - 1; // Convert range to segment tree indices
    bool reg = false; // Flag to check if the result is initialized
    int ans = 0; // Variable to store the result
    while (l <= r) {
        if (l % 2 == 1) { // If l is a right child
            propagateUpToX(l); // Propagate updates to the current node
            ans = reg ? min(ans, st[l]) : st[l]; // Update the result with the minimum value
            reg = true; // Mark the result as initialized
            l++; // Move to the next node
        }
        if (r % 2 == 0) { // If r is a left child
            propagateUpToX(r); // Propagate updates to the current node
            ans = reg ? min(ans, st[r]) : st[r]; // Update the result with the minimum value
            reg = true; // Mark the result as initialized
            r--; // Move to the previous node
        }
        l /= 2; // Move to the parent node
        r /= 2; // Move to the parent node
    }
    return ans; // Return the minimum value in the range
}

Conclusion

In this blog, we explored the implementation of an iterative segment tree, a powerful data structure for solving range queries and updates efficiently. We started by understanding the basics of segment trees, followed by step-by-step explanations of building the tree, querying ranges, and implementing lazy propagation for range updates. The iterative approach presented here is designed to be simple, efficient, and easy to implement in competitive programming scenarios.

Thank you for taking the time to read this blog. I hope it has been helpful in deepening your understanding of segment trees. If you have any questions, feedback, or suggestions, feel free to share them. Happy coding!!!

Full text and comments »

  • Vote: I like it
  • +10
  • Vote: I do not like it

By Thunder8971, history, 16 months ago, In English

After today's contest, jiangly reached a rating of 3977, just 23 points below 4000. Could jiangly be the next to reach a rating of 4000?

Full text and comments »

  • Vote: I like it
  • +73
  • Vote: I do not like it