Teach you how to get started with line segment tree from 0~

1. What is a segment tree?

1.1 Preliminary exploration of line segment tree

Definition: A line segment tree is aUsed to solve interval query problemsThe data structure is a generalized binary search tree .

Principle: It divides an interval into multiple smaller subintervals and stores some useful information for each subinterval , such as maximum, minimum, or sum.

Problems that can be solved: By summarizing the information stored in the interval step by step, the line segment tree can quickly answer various types of interval queries , such as summation, maximum value, minimum value, or updating the value of a certain interval.

**Time complexity:** The time complexity of line segment tree construction and query operation is O(logN) , where N is the size of the interval.

Restrictions: The problems that can be solved by the line segment tree must satisfy the interval addition . The interval addition means that for [L,R]the interval, its answer can be given by combining the answers of [L,M]and . [M+1,R]where M is the midpoint of the interval .

1.2 The difference between line segment tree and binary tree

For an array [1,2,3,4,5,6], its binary tree and line segment tree are shown in the figure below **"Interval subscripts start from 0"**:

It can be seen from the figure that the content stored in a single node in the binary tree is a value val, while the segment tree stores interval information .

Looking at the segment tree, we can also quickly draw the following conclusions:

  • The left and right children of each node store half of the node interval respectively
  • When the interval cannot be divided, get the leaf node

1.3 Subscript of line segment tree

Next, we add the subscript of the array to each node of the line segment tree , and the result is as follows:

It can be seen that in the line segment tree, when the subscript of a node is i, the subscript of its left child is 2 * i + 1, and the subscript of its right child is 2 * i + 2. At this time, we need to consider a question: when the size of the array is n, what should be the space of the line segment tree?

Answer: 2 * n - 1 , in a complete binary tree, the number of leaf nodes is equal to the number of non-leaf nodes minus one . In the line segment tree, the number of leaf nodes is equal to the array size n, and the number of non-leaf nodes is n - 1, so the space of the line segment tree should be 2 * n - 1 . Note that in order to facilitate calculation and prevent the array from crossing the boundary, we usually open the space size of the line segment tree to the smallest power of 2 larger than the total number of nodes, that is, the space of 4 *n size.

1.4 Storage content of line segment tree

Section 1.1 said: The line segment tree can quickly answer various types of interval queries , such as summation, maximum value, and minimum value. So in the summation , how is the line segment tree represented?

It can be seen that each leaf node stores an array subscript value val, and the sum value of each non-leaf node is equal to the sum of the storage values ​​of its left and right child nodes ; similarly, in the maximum\minimum value, non-leaf nodes store The value of is the larger\smaller value among its left and right child nodes .

2. The steps of line segment tree to solve the problem

2.1 Establishment

Although the information of a segment is stored in the line segment tree, we don't need to define a class to let it store the lvalue of the interval, the rvalue of the interval and the summation. Because we can create a line segment tree by means of recursion + subscript relationship , so that the nodes of the line segment tree only need to store the sum.

int nums[] = new int[]{
    
    1, 2, 3, 4, 5, 6};
int n = nums.length;
int[] segTree = new int[4 * n]; // 为线段树分配空间


void buildTree(int index, int left, int right) {
    
     // index 表示下标,left 表示左区间,right 表示右区间
    if (left == right) {
    
    
        segTree[index] = nums[left];
        return; // 到叶子节点就不能继续划分啦~
    }
    int mid = (left + right) / 2; // 一分为 2,例如将 [1,6] 划分为 [1,3] 和 [4,6]
    buildTree(2 * index + 1, left, mid); // 构建左子树,左孩子的下标为 2 * index + 1
    buildTree(2 * index + 2, mid + 1, right); // 构建右子树,右孩子的下标为 2 * index + 2
    segTree[index] = segTree[2 * index + 1] + segTree[2 * index + 2]; // 这里是求和,所以非叶子节点存储的值是左右孩子节点存储的值之和
}

public static void main(String[] args) {
    
    
        Solution solution = new Solution();
        solution.buildTree(0, 0, solution.nums.length - 1);
    }

2.2 Single point modification

Single-point modification is a special case of interval modification . Let's start with a simple single-point modification to see how the line segment tree is updated.

Idea: If we want to update the ivalue of the array x, then we can find ithe node from the root node that is equal to the left side of the interval and the right side of the interval, and modify its value. Then keep updating the value of its ancestor nodes on the way back .

public void update(int i, int value) {
    
    
    update(0, 0, nums.length - 1, i, value);
}

private void update(int index, int left, int right, int i, int value) {
    
     // i 表示要更新数组的下标,value 是更改后的值
    if (left == right) {
    
     // 当搜寻到叶子节点的时候,就可以修改了,前提是 i 在[0,2 * n - 2] 之间,下标从 0 开始算
        segTree[left] = value;
        return; // 赋值完就结束
    }
    int mid = (left + right) / 2;
    if (i <= mid) update(2 * index + 1, left, mid, i, value);
    else update(2 * index + 2, mid + 1, right, i, value);
    segTree[index] = segTree[index * 2 + 1] + segTree[index * 2 + 2]; // 更新祖先节点
}

2.3 Interval query with only single-point modification

Remember the usage condition of the line segment tree? Interval addition must be satisfied

Therefore, when we query an interval [a,b], we can split it into subintervals that satisfy interval addition. Or take the summation as an example : sum[1,5] = sum[1,3] + sum[4,5], sum[2,5] = sum[2,2] + sum[3,3] + sum[4,5].

public int query(int x, int y) {
    
    
        return query(0, 0, nums.length - 1, x, y);
    }

    private int query(int index, int left, int right, int x, int y) {
    
     // x 表示要查询的左区间,y 表示要查询的右区间
        if (x > right || y < left) return 0; // 如果查询区间在线段树区间外返回 0 
        if (x <= left && y >= right) return segTree[index];  // 当查询区间包含线段树区间,返回节点值
        int mid = (left + right) / 2;
        int leftQuery = query(2 * index + 1, left, mid, x, y); // 计算左孩子
        int rightQuery = query(2 * index + 2, mid + 1, right, x, y); // 计算右孩子
        return leftQuery + rightQuery; // 求和 
    }

2.4 Interval modification

When the content we need to modify is an interval instead of a single point, we cannot call the single point loop through the for loop, because this is no different from brute force cracking.

In order to solve this problem, we need to introduce a new concept: delayed marking , you can also call it lazy marking. The meaning of this mark is: the interval value marked by this mark has been updated, but its sub-interval has not been updated , and the updated information is the value stored in the mark .

Interval modifications that introduce deferred markers follow the following rules:

(1) If the interval to be modified completely covers the current interval, update this interval directly and mark it as delayed

(2) If it is not fully covered and there is a delay mark in the current interval, first download the delay mark to the sub-interval, and then clear the delay mark in the current interval.

(3) If the modified interval intersects with the left son, search for the left son; if it intersects with the right son, search for the right son

(4) Update the value of the current interval

Too many words, do you feel dizzy? It doesn't matter, let's use a specific example to see the interval modification~

[0,3] Add 1 to each number in the interval of the nums array , and the array will become [2,3,4,5,5,6]. In the line segment tree, we first visit the root node [0,5], the modification interval obviously does not completely cover the interval [0,5], and the current node does not have a delay mark; we look at the left child of the current node [0,2], obviously [0,3]and [0,2]There is an intersection, search the left child:
Next, we search [0,2]the left child Children, first [0,3]completely cover [0,2], then we update this interval, because the node records the sum, and we need to add 1 to each number in this interval, then the sum, sum = sum + 1 * 区间长度that is sum = 6 + (2 - 0 + 1) * 1 = 9; Next, we mark the node with a delay LazyTag = + 1, Indicates that the child nodes of this node have not been operated +1 .


After searching [0,2]the left child, we see that there is an intersection [0,3]with [0,5]the right child of [3,5], and we start to search for the right child: first, [0,3]it does not cover [3,5]and [3,5]there is no delay mark; we check [3,5]the left child and right child of respectively, and find that it [3,4]has an intersection with the left child, Start your search [3,4]:


We find [0,3]that is not completely covered [3,4], and [3,4]the node where does not contain a delay marker, we search its children. It is found that [0,3]and [3,3]have an intersection, and [0,3]are completely covered [3,3], we update the interval: sum = sum + 更新值 * 区间长度, ie sum = 4 + 1 * ( 3 - 3 + 1) = 5; and then mark the node as delayed LazyTag = +1. "It is precisely because the leaf nodes also have delay marks, and they need to continue to be distributed, which also requires twice the space, so 2 * n - 1 space is insufficient, and 4 * n space is needed"


So far, we have finished searching and started to update the interval value layer by layer:

At this point, if you are careful, you may find that the second rule of the delayed marking rule "If it is not completely covered and there is a delayed marking in the current interval, first download the delayed marking to the sub-interval , and then clear the delayed marking in the current interval" in the example just now There is no case where the delay mark is downloaded to the sub-interval . This is because we only performed one interval update, and this happens when we perform multiple interval updates~~
insert image description here

Suppose we want to [0,1]update the interval by adding one to all. First, determine the relationship between the root node interval and [0,1]the interval. [0,1]If it is not covered [0,5], we search [0,5]for the child nodes of ; if [0,1]it is partially covered [0,2], and [0,2]the node where is located has a delay mark, we perform the following operations:

  1. Download the delayed marker to its left and right child nodes
  2. Update the interval value of the left and right child nodes, [0,1]and update as sum = sum + 更新值 * 区间长度, ie sum = 3 + (1 - 0 + 1) * 1 = 5. right child nodesum = 3 + (2 - 2 + 1) * 1 = 4
  3. Reset the delay mark of the current node to 0, that is, there is no delay mark

The download delay flag is complete. Searching for nodes [0,2]that intersect with [0,1], we find that [0,2]is completely covered [0,1], directly updates [0,1]the interval equals 5+(1-0+1)=7, adds a delay mark LazyTag = + 1, and finally updates the interval value upwards :

For the convenience of memory, we abbreviate the steps of interval update as: complete coverage, partial coverage, "mark and download", search for children, and update interval. Let's take a look at its code implementation:

void pushUp(int index) {
    
    
    segTree[index] = segTree[index * 2 + 1] + segTree[index * 2 + 2]; // 向上更新,用孩子节点更新父节点
}

void pushDown(int index, int left, int right) {
    
     // 向下传递延迟标记
    if (lazyTag[index] != 0) {
    
    
        int mid = (left + right) / 2;
        lazyTag[index * 2 + 1] += lazyTag[index]; //更新左孩子的延迟标记
        lazyTag[index * 2 + 2] += lazyTag[index];//更新右孩子的延迟标记
        segTree[index * 2 + 1] += lazyTag[index] * (mid - left + 1); // 区间值 = sum + 更新值 *(区间长度)
        segTree[index * 2 + 2] += lazyTag[index] * (right - mid);
        lazyTag[index] = 0; // 清除延迟标记
    }
}
public void intervalUpdate(int x, int y, int value) {
    
    
    intervalUpdate(0, 0, nums.length - 1, x, y, value);
}
private void intervalUpdate(int index, int left, int right, int x, int y, int value) {
    
    
    if (x <= left && y >= right) {
    
     // 完全覆盖
        segTree[index] += value * (right - left + 1); // 更新区间值
        lazyTag[index] += value; // 更新延迟标记
        return;
    }
    pushDown(index, left, right); // 部分覆盖,下传延迟标记
    int mid = (left + right) / 2;
    if (x <= mid) intervalUpdate(index * 2 + 1, left, mid, x, y, value);
    if (y > mid) intervalUpdate(index * 2 + 2, mid + 1, right, x, y, value);
    pushUp(index);
}

2.5 Query based on interval modification

Queries based on range modifications are different because of deferred marking. It follows the following rules:

  • When the interval we query completely covers the node interval, just return the interval value directly
  • When partially covered, you need to download the delay mark first, and then query

Next, let's look at its code implementation~

public int query(int x, int y) {
    
    
        return query(0, 0, nums.length - 1, x, y);
}
private int query(int index, int left, int right, int x, int y) {
    
     // x 表示要查询的左区间,y 表示要查询的右区间
    if (x > right || y < left) return 0; // 如果查询区间在线段树区间外返回 0
    if (x <= left && y >= right) return segTree[index];  // 当查询区间包含线段树区间,返回节点值
    pushDown(index,left,right); //下传延迟标记
    int mid = (left + right) / 2;
    int leftQuery = query(2 * index + 1, left, mid, x, y); // 计算左孩子
    int rightQuery = query(2 * index + 2, mid + 1, right, x, y); // 计算右孩子
    return leftQuery + rightQuery; // 求和
}

Compared with the query in Section 2.3, we found that it only adds a manipulation of the download delay mark after fully covering this step~

3. Complete code

Finally, attach the complete code and test data:

import java.util.List;

public class Solution {
    
    
    int nums[] = new int[]{
    
    1, 2, 3, 4, 5, 6};
    int n = nums.length;
    int[] segTree = new int[4 * n]; // 为线段树分配空间
    int lazyTag[] = new int[4 * n]; // 为延迟标记分配空间

    void buildTree(int index, int left, int right) {
    
     // index 表示下标,left 表示左区间,right 表示右区间
        if (left == right) {
    
    
            segTree[index] = nums[left];
            return; // 到叶子节点就不能继续划分啦~
        }
        int mid = (left + right) / 2; // 一分为 2,例如将 [1,6] 划分为 [1,3] 和 [4,6]
        buildTree(2 * index + 1, left, mid); // 构建左子树,左孩子的下标为 2 * index + 1
        buildTree(2 * index + 2, mid + 1, right); // 构建右子树,右孩子的下标为 2 * index + 2
        segTree[index] = segTree[2 * index + 1] + segTree[2 * index + 2]; // 这里是求和,所以非叶子节点存储的值是左右孩子节点存储的值之和
    }

    void pushUp(int index) {
    
    
        segTree[index] = segTree[index * 2 + 1] + segTree[index * 2 + 2]; // 向上更新,用孩子节点更新父节点
    }

    void pushDown(int index, int left, int right) {
    
     // 向下传递延迟标记
        if (lazyTag[index] != 0) {
    
    
            int mid = (left + right) / 2;
            lazyTag[index * 2 + 1] += lazyTag[index]; //更新左孩子的延迟标记
            lazyTag[index * 2 + 2] += lazyTag[index];//更新右孩子的延迟标记
            segTree[index * 2 + 1] += lazyTag[index] * (mid - left + 1); // 区间值 = sum + 更新值 *(区间长度)
            segTree[index * 2 + 2] += lazyTag[index] * (right - mid);
            lazyTag[index] = 0; // 清除延迟标记
        }
    }

    public void intervalUpdate(int x, int y, int value) {
    
    
        intervalUpdate(0, 0, nums.length - 1, x, y, value);
    }

    private void intervalUpdate(int index, int left, int right, int x, int y, int value) {
    
    
        if (x <= left && y >= right) {
    
     // 完全覆盖
            segTree[index] += value * (right - left + 1); // 更新区间值
            lazyTag[index] += value; // 更新延迟标记
            return;
        }
        pushDown(index, left, right); // 部分覆盖,下传延迟标记
        int mid = (left + right) / 2;
        if (x <= mid) intervalUpdate(index * 2 + 1, left, mid, x, y, value);
        if (y > mid) intervalUpdate(index * 2 + 2, mid + 1, right, x, y, value);
        pushUp(index);
    }

    // 区间查询
    public int query(int x, int y) {
    
    
        return query(0, 0, nums.length - 1, x, y);
    }

    private int query(int index, int left, int right, int x, int y) {
    
     // x 表示要查询的左区间,y 表示要查询的右区间
        if (x > right || y < left) return 0; // 如果查询区间在线段树区间外返回 0
        if (x <= left && y >= right) return segTree[index];  // 当查询区间包含线段树区间,返回节点值
        pushDown(index, left, right); //下传延迟标记
        int mid = (left + right) / 2;
        int leftQuery = query(2 * index + 1, left, mid, x, y); // 计算左孩子
        int rightQuery = query(2 * index + 2, mid + 1, right, x, y); // 计算右孩子
        return leftQuery + rightQuery; // 求和
    }

    public static void main(String[] args) {
    
    
        Solution solution = new Solution();
        solution.buildTree(0, 0, solution.nums.length - 1);
        solution.intervalUpdate(0,3,1);
        solution.intervalUpdate(1,2,1);
        System.out.println(solution.query(0, 2));
    }
}

Can see the end, you are really great, come on~


Guess you like

Origin blog.csdn.net/jiaweilovemingming/article/details/131992995