【模版】线段树


线段树


线段树是一种普通的高等树状数据结构。
基础的线段树可以解决 区间加减修改 ,查询 区间和 极值
线段树是一棵二叉树,每个节点一定有两个或无孩子。
线段树传递参数太多,导致耗时加大,运行时间大于预估时间复杂度。

建树复杂度: O ( N l o g 2 N ) O(N*log_2N)
每个节点可以保存区间信息,那么左孩子可以保存前半个区间的信息,右孩子保存后半个区间的信息。两个孩子所记录的信息无交集 且 并集 等于 父亲所保存的信息。(信息即 元素)。当该节点只储存单个元素信息时,就不必再创造左右节点。
根据二叉树的特点,对每个节点编号。1号为祖先,储存整个区间的信息。
那么对于每个孩子,结合二叉树的特点,左孩子的编号是父亲的编号 2 *2 ,右孩子的编号是父亲的编号 2 + 1 *2+1 。用这样的编号方式,编号数字不重不漏,最大化利用空间。递归建树法。

build_tree(1, 1, n);
void build_tree(int cur, int l, int r)
{
    if(l == r)
    {
        tree[cur] = read();
        return;
    }
    int mid = l + r >> 1;
    build_tree(cur<<1, l, mid);
    build_tree(cur<<1|1, mid+1, r);
    tree[cur] = tree[cur<<1] + tree[cur<<1|1];
    return;
}

查询复杂度: O ( l o g 2 N ) O(log_2N)
查询 区间极值 或者 计算 区间和。是用分块的形式。
m a i n main 函数中查询 q u e r y _ s u m ( 1 , 1 , n , l , r ) query\_sum(1, 1, n, l, r) ,分别表示:
点的编号,当前区间左区间,当前区间右区间,查询的左区间,查询的右区间。(从 1 1 号点开始查询,所以当前区间是 [ 1 , n ] [1,n] )
查询有三种情况:
1 1 .假设总区间是 [ 1 , 10 ] [1,10] ,如果查询 [ 1 , 5 ] [1,5] ,那么显然该查询当前区间的前一半,那么在递归调用的时候,下一步查询的就是 q u e r y ( c u r 2 , l , m i d , L , R ) query(cur*2, l, mid, L, R)
2 2 .假设总区间是 [ 1 , 10 ] [1,10] ,如果查询 [ 6 , 10 ] [6,10] ,那么显然该查询当前区间的后一半,那么在递归调用的时候,下一步查询的就是 q u e r y ( c u r 2 + 1 , m i d + 1 , r , L , R ) query(cur*2+1, mid+1, r, L, R)
3 3 .假设总区间是 [ 1 , 10 ] [1,10] ,如果查询 [ 4 , 10 ] [4,10] ,那么显然前两种方法都无法做到正确查询。这时候需要把当前的区间分裂成两个长度相等部分,查询的区间以当前区间的 m i d mid ,分成两部分。前半部分对应,后半部分也对应。
所以总区间的前部分 分成 [ 1 , 5 ] [1,5] ,查询变成了 [ 4 , 5 ] [4,5] 。后部分分成 [ 6 , 10 ] [6,10] ,查询变成了 [ 6 , 10 ] [6,10] 。然后分别递归,直到满足可以用查询的 1 , 2 1,2 才停止递归。 q u e r y s u m ( c u r 2 , l , m i d , L , m i d ) , q u e r y s u m ( c u r 2 + 1 , m i d + 1 , r , m i d + 1 , R ) ; query_sum(cur*2, l, mid, L, mid), query_sum(cur*2+1, mid+1, r, mid+1, R);

query_sum(1, 1, n, l, r);
void query_sum(int cur, int l, int r, int L, int R)
{
    if(L <= l && r <= R)
    {
        ans += tree[cur];
        return;
    }
    if(lazy[cur])   pushdown(cur, l, r);
    int mid = l + r >> 1;
    if(R <= mid)    query_sum(cur<<1, l, mid, L, R);
    else if(L > mid)    query_sum(cur<<1|1, mid+1, r, L, R);
    else    query_sum(cur<<1, l, mid, L, mid), query_sum(cur<<1|1, mid+1, r, mid+1, R);    
    tree[cur] = tree[cur<<1] + tree[cur<<1|1];
}

区间修改复杂度: O ( l o g 2 N ) O(log_2N)
区间加减也是用的分块的形式。
给一个区间每个数加 w w , 需要具体落实到每一个元素吗?不需要。(需要的话干嘛还要用线段树)
同“查询区间和”的代码长得几乎一样。如果当前区间 是 要加的区间 的子集,那么当前的和 直接 + ( r l + 1 ) w +(r-l+1)*w 。就修改好当前点保存的区间的和的信息了。但是如果要查询该子集的子集,那么该子集的子集并没有更新啊,因为子集更新好了新的元素和,子集的子集并没有与更新元素和。所以用一个数组 l a z y lazy 。如果某个子集执行了更新和的操作,那么给这个点打一个lazy标记。如果以后要查询该点的子集,就在查询前进行 p u s h d o w n pushdown 。 把当前点的标记更新给要用的区间,实现对子集的子集的更新。复杂度约等于 O ( 1 ) O(1)
顺便附上 p u s h d o w n pushdown 代码

update(1, 1, n, l, r, w);
void update(int cur, int l, int r, int L, int R, int w)
{
    if(L <= l && r <= R)
    {
        tree[cur] += (r-l+1) * w;
        lazy[cur] += w;
        return;
    }
    if(lazy[cur])   pushdown(cur, l, r);
    int mid = l + r >> 1;
    if(R <= mid)    update(cur<<1, l, mid, L, R, w);
    else if(L > mid)    update(cur<<1|1, mid+1, r, L, R, w);
    else    update(cur<<1, l, mid, L, mid, w), update(cur<<1|1, mid+1, r, mid+1, R, w);    
    tree[cur] = tree[cur<<1] + tree[cur<<1|1];
}
void pushdown(int cur, int l, int r)
{
    int mid = l + r >> 1;
    tree[cur<<1] += (mid-l+1) * lazy[cur];
    tree[cur<<1|1] += (r-mid) * lazy[cur];
    lazy[cur<<1] += lazy[cur];
    lazy[cur<<1|1] += lazy[cur];
    lazy[cur] = 0;
}

线段树完整代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <cstdlib>
using namespace std;
int n,m,ans;
int tree[400400],lazy[400400];
int read()
{
    int rt = 0, in = 1; char ch = getchar();
    while(ch < '0' || ch > '9') {if(ch == '-') in = -1; ch = getchar();}
    while(ch >= '0' && ch <= '9') {rt = rt * 10 + ch - '0'; ch = getchar();}
    return rt * in;
}

void print(int x)
{
    if(x < 0) putchar('-'),  x = -x;
    if(x > 9) print(x / 10);
    putchar(x % 10 + '0');
}

void build_tree(int cur, int l, int r)
{
    if(l == r)
    {
        tree[cur] = read();
        return;
    }
    int mid = l + r >> 1;
    build_tree(cur<<1, l, mid);
    build_tree(cur<<1|1, mid+1, r);
    tree[cur] = tree[cur<<1] + tree[cur<<1|1];
    return;
}
void pushdown(int cur, int l, int r)
{
    int mid = l + r >> 1;
    tree[cur<<1] += (mid-l+1) * lazy[cur];
    tree[cur<<1|1] += (r-mid) * lazy[cur];
    lazy[cur<<1] += lazy[cur];
    lazy[cur<<1|1] += lazy[cur];
    lazy[cur] = 0;
}
void update(int cur, int l, int r, int L, int R, int w)
{
    if(L <= l && r <= R)
    {
        tree[cur] += (r-l+1) * w;
        lazy[cur] += w;
        return;
    }
    if(lazy[cur])   pushdown(cur, l, r);
    int mid = l + r >> 1;
    if(R <= mid)    update(cur<<1, l, mid, L, R, w);
    else if(L > mid)    update(cur<<1|1, mid+1, r, L, R, w);
    else    update(cur<<1, l, mid, L, mid, w), update(cur<<1|1, mid+1, r, mid+1, R, w);    
    tree[cur] = tree[cur<<1] + tree[cur<<1|1];
}
void query_sum(int cur, int l, int r, int L, int R)
{
    if(L <= l && r <= R)
    {
        ans += tree[cur];
        return;
    }
    if(lazy[cur])   pushdown(cur, l, r);
    int mid = l + r >> 1;
    if(R <= mid)    query_sum(cur<<1, l, mid, L, R);
    else if(L > mid)    query_sum(cur<<1|1, mid+1, r, L, R);
    else    query_sum(cur<<1, l, mid, L, mid), query_sum(cur<<1|1, mid+1, r, mid+1, R);    
    tree[cur] = tree[cur<<1] + tree[cur<<1|1];
}
int main()
{
    n = read(), m = read();
    build_tree(1,1,n);
    for(int i = 1; i <= m; i++)
    {
        int ins = read();
        if(ins == 1)
        {
            int l = read(), r = read(), w = read();
            update(1, 1, n, l, r, w);
        }
        if(ins == 2)
        {
            int l = read(), r = read();
            ans = 0;
            query_sum(1, 1, n, l, r);
            print(ans),putchar('\n');
        }
    }
    return 0;
}

例题:
[模版]线段树
luogu P1637
luogu P4145
多写写线段树,就会用还不会写错了

发布了10 篇原创文章 · 获赞 10 · 访问量 674

猜你喜欢

转载自blog.csdn.net/LH_991215/article/details/104217847