线段树

线段树的概念

 可以发现,每个叶子结点的值就是原数组的值,每个非叶子结点的度都为二,且左右两个孩子分别存储父亲一半的区间。每个父亲的存储的值也就是两个孩子存储的值的最大值。(概念讲解以区间最值版为例,如果时区间和版,每个父亲的存储的值也就是两个孩子存储的值的和

线段树的实现方式

线段树的实现方式之一就是使用数组的方式,用2*i表示第i个节点的左子树,2 * i+1表示第i个节点的右子树。其中绿色的数字就是数组的下标。

相对于树状数组:这里的原数组A[] = {0,1,8,6,4,3,5};线段树数组c[] = {0 8 8 5 4 5 6 7 8 9 0 0 12 13}(二叉树的层序遍历)数组数据的记录从1开始(后序讲解都使用该例子)

一般线段树数组大小为原数组的4倍

 

基于求区间最值的线段树

线段树的建立

void build(int k, int l, int r)
{
    if (l == r)//如果是最后的叶子节点,就直接赋值
        c[k] = a[l];
    else
    {
        int m = (l + r) / 2;
        build(2*k, l, m);//不是的话就递归
        build(2*k+1, m + 1, r);
        c[k] = max(c[k * 2], c[k * 2 + 1]);//由于不是最后的叶子节点,所以一定有左右子节点,所以此节点的值就是左右子节点较大的值
    }
}

左移右移版:

void build(int k, int l, int r)
{
    if (l == r)//如果是最后的叶子节点,就直接赋值
        c[k] = a[l];
    else
    {
        int m = l+((r-l)>>1);//注意这里的括号,进行优先级的处理
        build(k<<1, l, m);//不是的话就递归
        build(k<<1|1, m + 1, r);
        c[k] = max(c[k<<1], c[k<<1|1]);//由于不是最后的叶子节点,所以一定有左右子节点,所以此节点的值就是左右子节点较大的值
    }
}

线段树的单点更新

 如图:若给原数组a[3]+7,紫色为需要更新的数据

void update(int index,int v,int l,int r,int k) //index为下标,v为要加上的值,
{
    if (l == r)//左端点等于右端点,即为叶子结点,直接加上v即可
    {
        a[l] += v; c[k] += v;//原数组和线段树数组都得到更新
    }
    else
    {
        int m = l + ((r - l) >> 1);//m则为中间点,左儿子的结点区间为[l,m],右儿子的结点区间为[m+1,r]
        if (index <= m)//如果需要更新的结点在左子树区间
            update(index, v, l, m, k<<1);
        else //如果需要更新的结点在右子树区间
            update(index, v, m + 1, r, k<<1|1);
        c[k] = max(c[k << 1], c[k << 1 | 1]);//更新父节点的值
    }
}

线段树的区间查询

比如现在我要查询[2,5]区间的最值,一共有5个区间,而且我们可以发现[4,5]这个区间已经包含了两个子树的信息,所以我们需要查询的区间只有三个,分别是[2,2],[3,3],[4,5]

int query(int L,int R,int l,int r,int k)//[L,R]即为要查询的区间,l,r为结点区间,k为结点下标
{
    if (L <= l&&R >= r) //如果当前结点的区间真包含于要查询的区间内,则返回结点信息且不需要往下递归
        return c[k];
    else
    {
        int res = -INF;
        int m = l + ((r - l) >> 1);//m则为中间点,左儿子的结点区间为[l,m],右儿子的结点区间为[m+1,r]
        if (L <= m)res = max(res, query(L, R, l, m, k << 1));//如果左子树和需要查询的区间交集非空
        if (m < R)res = max(res, query(L, R, m + 1, r, k << 1 | 1));//如果右子树和需要查询的区间交集非空,注意这里不是else if,因为查询区间可能同时和左右区间都有交集
        return res;//返回当前结点得到的信息
    }
}

代码汇总

//6
//1 8 6 4 3 5
#include<iostream>
#include<algorithm>
using namespace std;
#define MAX 100
#define INF 0x3f3f3f3f
int a[MAX], c[MAX <<2];//空间,一般会开到4*n的空间防止RE。
void build(int k, int l, int r)
{
    if (l == r)//如果是最后的叶子节点,就直接赋值
        c[k] = a[l];
    else
    {
        int m = l+((r-l)>>1);//注意这里的括号,进行优先级的处理
        build(k<<1, l, m);//不是的话就递归
        build(k<<1|1, m + 1, r);
        c[k] = max(c[k<<1], c[k<<1|1]);//由于不是最后的叶子节点,所以一定有左右子节点,所以此节点的值就是左右子节点较大的值
    }
}

void update(int index,int v,int l,int r,int k)
{
    if (l == r)
    {
        a[l] += v; c[k] += v;
    }
    else
    {
        int m = l + ((r - l) >> 1);
        if (index <= m)
            update(index, v, l, m, k<<1);
        else
            update(index, v, m + 1, r, k<<1|1);
        c[k] = max(c[k << 1], c[k << 1 | 1]);
    }
}
int query(int L,int R,int l,int r,int k)
{
    if (L <= l&&R >= r)
        return c[k];
    else
    {
        int res = -INF;
        int m = l + ((r - l) >> 1);
        if (L <= m)res = max(res, query(L, R, l, m, k << 1));
        if (m < R)res = max(res, query(L, R, m + 1, r, k << 1 | 1));
        return res;
    }
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    build(1, 1, n);
    printf("更改前的线段树数组c[]:");
    for (int i = 1; i <= (n<<1|1); i++)
        printf("%s%d", i == 1 ? "" : " ", c[i]);
    printf("
");
    printf("输出【2,5】的最大值:");
    printf("%d", query(2, 5, 1, n, 1));
    update(3, 7, 1, n, 1);
    printf("
");
    printf("更改后的线段树数组c[]:");
    for (int i = 1; i <= (n << 1|1); i++)
        printf("%s%d", i == 1 ? "" : " ", c[i]);
    printf("
");
    printf("输出【2,5】的最大值:");
    printf("%d", query(2,5,1,n,1));
}

线段树的区间更新

线段树的区间更新引进了懒惰标记的概念

正常的区间更新,比如下图中【2,5】都要+1,则a[2] a[3] a[4] a[5] 都要加 1,则在下图中需要更新很多节点,为了提高更新的效率,每次更新只更新到更新区间完全覆盖线段树结点区间为止

所以标记的含义:本区间已经被更新过了,但是子区间却没有被更新过

void pushdown(int k)
{
    if (lazy[k])//如果有lazy标记
    {
        lazy[k << 1] += lazy[k];//更新左子树的lazy值
        lazy[k << 1|1] += lazy[k];//更新右子树的lazy值
        c[k << 1] += lazy[k];//左子树的最值加上lazy值
        c[k << 1 | 1]+=lazy[k];  //右子树的最值加上lazy值
        lazy[k] = 0;//lazy值归0
    }
}
void update(int L,int R, int v, int l, int r, int k)
{
    if (L <= l&&R >= r) //如果当前结点的区间真包含于要更新的区间内
    {
        lazy[k] += v; //懒惰标记
        c[k] += v; //最大值加上v之后,此区间的最大值也肯定是加v
    }
    else
    {
        pushdown(k);//查询lazy标记,更新子树
        int m = l + ((r - l) >> 1);
        if (L <= m)
            update(L, R, v, l, m,k<<1);
        if (R > m)
            update(L, R, v, m + 1, r, k << 1 | 1);
        c[k] = max(c[k << 1], c[k << 1 | 1]);
    }
    
}

线段树的区间查询

int query(int L,int R,int l,int r,int k)
{
    if (L <= l&&R >= r)
        return c[k];
    else
    {
        pushdown(k); /*每次都需要更新子树的Lazy标记*/
        int res = -INF;
        int m = l + ((r - l) >> 1);
        if (L <= m)res = max(res, query(L, R, l, m, k << 1));
        if (m < R)res = max(res, query(L, R, m + 1, r, k << 1 | 1));
        return res;
    }
}

代码汇总

#include<iostream>
#include<algorithm>
using namespace std;
#define MAX 100
#define INF 0x3f3f3f3f
int a[MAX], c[MAX << 2], lazy[MAX << 2];//空间,一般会开到4*n的空间防止RE。
void build(int k, int l, int r)
{
    if (l == r)//如果是最后的叶子节点,就直接赋值
        c[k] = a[l];
    else
    {
        int m = l + ((r - l) >> 1);//注意这里的括号,进行优先级的处理
        build(k << 1, l, m);//不是的话就递归
        build(k << 1 | 1, m + 1, r);
        c[k] = max(c[k << 1], c[k << 1 | 1]);//由于不是最后的叶子节点,所以一定有左右子节点,所以此节点的值就是左右子节点较大的值
    }
}

void pushdown(int k)
{
    if (lazy[k])
    {
        lazy[k << 1] += lazy[k];
        lazy[k << 1|1] += lazy[k];
        c[k << 1] += lazy[k];
        c[k << 1 | 1]+=lazy[k];
        lazy[k] = 0;
    }
}
void update(int L,int R, int v, int l, int r, int k)
{
    if (L <= l&&R >= r)
    {
        lazy[k] += v;
        c[k] += v;
    }
    else
    {
        pushdown(k);
        int m = l + ((r - l) >> 1);
        if (L <= m)
            update(L, R, v, l, m,k<<1);
        if (R > m)
            update(L, R, v, m + 1, r, k << 1 | 1);
        c[k] = max(c[k << 1], c[k << 1 | 1]);
    }
    
}
int query(int L,int R,int l,int r,int k)
{
    if (L <= l&&R >= r)
        return c[k];
    else
    {
        pushdown(k); /**每次都需要更新子树的Lazy标记*/
        int res = -INF;
        int m = l + ((r - l) >> 1);
        if (L <= m)res = max(res, query(L, R, l, m, k << 1));
        if (m < R)res = max(res, query(L, R, m + 1, r, k << 1 | 1));
        return res;
    }
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    build(1, 1, n);
    printf("更改前的线段树数组c[]:");
    for (int i = 1; i <= (n << 1 | 1); i++)
        printf("%s%d", i == 1 ? "" : " ", c[i]);
    printf("
");
    printf("输出【2,5】的最大值:");
    printf("%d", query(2, 5, 1, n, 1));
    update(2, 5,1, 1, n, 1);
    printf("
");
    printf("更改后的线段树数组c[]:");
    for (int i = 1; i <= (n << 1 | 1); i++)
        printf("%s%d", i == 1 ? "" : " ", c[i]);
    printf("
");
    printf("输出【2,5】的最大值:");
    printf("%d", query(2, 5, 1, n, 1));
}


基于求区间和的线段树

线段树的建立

void build(int k, int l, int r)
{
    if (l == r)//如果是最后的叶子节点,就直接赋值
        c[k] = a[l];
    else
    {
        int m = l + ((r - l) >> 1);//注意这里的括号,进行优先级的处理
        build(k << 1, l, m);//不是的话就递归
        build(k << 1 | 1, m + 1, r);
        c[k] = c[k << 1]+c[k << 1 | 1];//这里改为相加
    }
}

线段树的单点更新

void update(int index, int v, int l, int r, int k)
{
    if (l == r)
    {
        a[l] += v; c[k] += v;
    }
    else
    {
        int m = l + ((r - l) >> 1);
        if (index <= m)
            update(index, v, l, m, k << 1);
        else
            update(index, v, m + 1, r, k << 1 | 1);
        c[k] = c[k << 1]+ c[k << 1 | 1];//这里变为相加
    }
}

线段树的区间查询

int query(int L, int R, int l, int r, int k)
{
    if (L <= l&&R >= r)
        return c[k];
    else
    {
        int res = 0;//这里
        int m = l + ((r - l) >> 1);
        if (L <= m)res +=  query(L, R, l, m, k << 1);//这里
        if (m < R)res += query(L, R, m + 1, r, k << 1 | 1);//这里
        return res;
    }
}

代码汇总

#include<iostream>
#include<algorithm>
using namespace std;
#define MAX 100
#define INF 0x3f3f3f3f
int a[MAX], c[MAX << 2];//空间,一般会开到4*n的空间防止RE。
void build(int k, int l, int r)
{
    if (l == r)//如果是最后的叶子节点,就直接赋值
        c[k] = a[l];
    else
    {
        int m = l + ((r - l) >> 1);//注意这里的括号,进行优先级的处理
        build(k << 1, l, m);//不是的话就递归
        build(k << 1 | 1, m + 1, r);
        c[k] = c[k << 1]+c[k << 1 | 1];//由于不是最后的叶子节点,所以一定有左右子节点,所以此节点的值就是左右子节点较大的值
    }
}

void update(int index, int v, int l, int r, int k)
{
    if (l == r)
    {
        a[l] += v; c[k] += v;
    }
    else
    {
        int m = l + ((r - l) >> 1);
        if (index <= m)
            update(index, v, l, m, k << 1);
        else
            update(index, v, m + 1, r, k << 1 | 1);
        c[k] = c[k << 1]+ c[k << 1 | 1];
    }
}
int query(int L, int R, int l, int r, int k)
{
    if (L <= l&&R >= r)
        return c[k];
    else
    {
        int res = 0;
        int m = l + ((r - l) >> 1);
        if (L <= m)res +=  query(L, R, l, m, k << 1);
        if (m < R)res += query(L, R, m + 1, r, k << 1 | 1);
        return res;
    }
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    build(1, 1, n);
    printf("更改前的线段树数组c[]:");
    for (int i = 1; i <= (n << 1 | 1); i++)
        printf("%s%d", i == 1 ? "" : " ", c[i]);
    printf("
");
    printf("输出【2,5】的和:");
    printf("%d", query(2, 5, 1, n, 1));
    update(3, 7, 1, n, 1);
    printf("
");
    printf("更改后的线段树数组c[]:");
    for (int i = 1; i <= (n << 1 | 1); i++)
        printf("%s%d", i == 1 ? "" : " ", c[i]);
    printf("
");
    printf("输出【2,5】的和:");
    printf("%d", query(2, 5, 1, n, 1));
}

线段树的区间更新

void pushdown(int k,int l,int r)
{
    if (lazy[k])
    {
        int m = (l + r) >> 1;
        lazy[k << 1] += lazy[k];
        lazy[k << 1 | 1] += lazy[k];
        c[k << 1] += lazy[k]*(m-l+1);//注意这里
        c[k << 1 | 1] += lazy[k]*(r-(m+1)+1);//注意这里
        lazy[k] = 0;
    }
}
void update(int L, int R, int v, int l, int r, int k)
{
    if (L <= l&&R >= r)//如果被操作区间完全包含,就不用往下更新了
    {
        c[k] +=(r-l+1)* v;//注意这里
        lazy[k] += v;
    }
    else
    {
        pushdown(k,l,r);
        int m = l + ((r - l) >> 1);
        if (L <= m)
            update(L, R, v, l, m, k << 1);
        if (R >m)
            update(L, R, v, m + 1, r, k << 1 | 1);
        c[k] = c[k << 1]+ c[k << 1 | 1];
    }

}

该区间更新如果对原数组的【2,5】依次加一:

 

 应该是这样,但其实不是的,结果其实是这样的:

原因是这样的:

当递归到初始右边节点7的时候已经满足if (L <= l&&R >= r),即当前结点的区间真包含于要更新的区间内,所以此时直接c[k] +=(r-l+1)* v;计算好节点7下面的子节点个数,一个节点+v,总共就要加(r-l+1)* v,所以节点3 4 就不用加一更新了。

这里求区间和与求区间最值有点小区别:

例如上述情况:求区间和时7是加(r-l+1)* v,而求区间最值的时候子节点都加v,所以最大值也加v,就不存在求子节点个数的问题。

求区间和与求区间最值中pushdown函数也是一样的道理。

 

//求区间和的update中的判断
if (L <= l&&R >= r)
    {
        c[k] +=(r-l+1)* v;//注意这里
        lazy[k] += v;
    }
//求区间最值的update中的判断
    if (L <= l&&R >= r) //如果当前结点的区间真包含于要更新的区间内
    {
        lazy[k] += v; //懒惰标记
        c[k] += v; //最大值加上v之后,此区间的最大值也肯定是加v
    }

线段树的区间查询

int query(int L, int R, int l, int r, int k)
{
    if (L <= l&&R >= r)
        return c[k];
    else
    {
        pushdown(k,l,r); /*每次都需要更新子树的Lazy标记*///这里改变了
        int res = 0;
        int m = l + ((r - l) >> 1);
        if (L <= m)res += query(L, R, l, m, k << 1);
        if (m < R)res +=query(L, R, m + 1, r, k << 1 | 1);
        return res;
    }
}

代码汇总

#include<iostream>
#include<algorithm>
using namespace std;
#define MAX 100
#define INF 0x3f3f3f3f
int a[MAX], c[MAX << 2], lazy[MAX << 2];//空间,一般会开到4*n的空间防止RE。
void build(int k, int l, int r)
{
    if (l == r)//如果是最后的叶子节点,就直接赋值
        c[k] = a[l];
    else
    {
        int m = l + ((r - l) >> 1);//注意这里的括号,进行优先级的处理
        build(k << 1, l, m);//不是的话就递归
        build(k << 1 | 1, m + 1, r);
        c[k] = c[k << 1]+ c[k << 1 | 1];//由于不是最后的叶子节点,所以一定有左右子节点,所以此节点的值就是左右子节点较大的值
    }
}

void pushdown(int k,int l,int r)
{
    if (lazy[k])
    {
        int m = (l + r) >> 1;
        lazy[k << 1] += lazy[k];
        lazy[k << 1 | 1] += lazy[k];
        c[k << 1] += lazy[k]*(m-l+1);
        c[k << 1 | 1] += lazy[k]*(r-(m+1)+1);
        lazy[k] = 0;
    }
}
void update(int L, int R, int v, int l, int r, int k)
{
    if (L <= l&&R >= r)
    {
        c[k] +=(r-l+1)* v;
        lazy[k] += v;
    }
    else
    {
        pushdown(k,l,r);
        int m = l + ((r - l) >> 1);
        if (L <= m)
            update(L, R, v, l, m, k << 1);
        if (R >m)
            update(L, R, v, m + 1, r, k << 1 | 1);
        c[k] = c[k << 1]+ c[k << 1 | 1];
    }

}
int query(int L, int R, int l, int r, int k)
{
    if (L <= l&&R >= r)
        return c[k];
    else
    {
        pushdown(k,l,r); /**每次都需要更新子树的Lazy标记*/
        int res = 0;
        int m = l + ((r - l) >> 1);
        if (L <= m)res += query(L, R, l, m, k << 1);
        if (m < R)res +=query(L, R, m + 1, r, k << 1 | 1);
        return res;
    }
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    build(1, 1, n);
    printf("更改前的线段树数组c[]:");
    for (int i = 1; i <= (n << 1 | 1); i++)
        printf("%s%d", i == 1 ? "" : " ", c[i]);
    printf("
");
    printf("输出【2,5】的和:");
    printf("%d", query(2, 5, 1, n, 1));
    update(2, 5, 1, 1, n, 1);//2~5都加1
    printf("
");
    printf("更改后的线段树数组c[]:");
    for (int i = 1; i <= (n << 1 | 1); i++)
        printf("%s%d", i == 1 ? "" : " ", c[i]);
    printf("
");
    printf("输出【2,5】的和:");
    printf("%d", query(2, 5, 1, n, 1));
}

 参考:https://www.cnblogs.com/xenny/p/9801703.html

原文地址:https://www.cnblogs.com/Jason66661010/p/12800443.html