线段树


 

总原理:

将[1,n]分解成若干特定的子区间(数量不超过4*n)

用线段树对“编号连续”的一些点,进行修改或者统计操作,修改和统计的复杂度都是O(log2(n))

用线段树统计的东西,必须符合区间加法,(也就是说,如果已知左右两子树的全部信息,比如要能够推出父节点);否则,不可能通过分成的子区间来得到[L,R]的统计结果。

一个问题,只要能化成对一些“连续点”的修改和统计问题,基本就可以用线段树来解决了

 

 

 

注意:区分3个概念:原数组下标,线段树中的下标和存储下标。

原数组下标,这里都默认下标从1开始(一般用a数组表示,a [1] , a[2] …… a [n] )

线段树下标,是指该节点所管理的局部点集区,。一般都用区间 [ l , r ] 表示,即这一点存储的是  a [l] ~ a [r]  区间内的局部解、局部信息。

存储下标,是指该元素在总内存池 arr 中实际的存储位置,一般用 arr [ root ]表示。存储方式类似于“完全二叉树”,详细见下面的图

 

注意,在元数个数N确定的时候,数字对 [ l , r ] 的值 和 root 的值,是对应的。

每一个arr[root],都有一个对应的、专属于它自己管理的区间 [ l , r ]

因为对于确定的N,构建的线段树是唯一确定的

具体落实到计算上,就是 l、r 和 root同步变化,例如:(l , mid , root*2) 、 (mid+1 , r , root*2+1)

 

 

线段树的解题关键:

1:要推出父节点,我需要知道两个子节点的那些信息?————这个决定了, 每个单位结点的数据结构(需要设置哪些成员变量),如何设置node

2:在1的基础上,已知两个子节点的完整信息,我该如何推出父节点?————定义push_up函数

3:如果是区间更新,还需要思考:如何设置lazytag?如何用设置的lazy_tag更新所需数据值?

     Lazytag的设置,主要是要求:求sum的时候,根据从父区间传下来的lazy_tag,能正确更新子区间所有内部数据值和他的lazy值,从而正确求出sum

 

 

 

线段树的存储结构:

假设有某个结点,它的信息存储在 arr [x] 中,那么它的左子节点信息存储在 arr [2x] 中,右结点信息存储在 arr [2x+1]中

并且,arr [1] 为整个树的根节点 ,所以,整体的统计信息、最终的答案,是存在节点1中的。

 

 

 

线段树的代码组成:

1:build函数:用于建立整个线段树。

2:upgrade,修改(更新)函数。注意区分:单点更新、区间更新。

3:push_up: 父节点回溯更新函数

如果是区间更新,还需要:push_down,用于:把lazy_tag往下推一级,推到该节点的子节点,并更新该节点左右子节点的:数据值、lazy_tag值。

 

 

一、build函数:构建线段树

原理:

开始时是区间[1,n] ,通过递归来逐步分解;

线段树对于每个n的分解是唯一的,所以n相同的线段树,结构相同。

 

先向下递归地寻找叶节点所在位置(寻找长度为1的区间),

一旦找到就把值放到这个位置,

并向上回溯地修改所有父节点的值。

 

 

递归遍历的顺序,十分类似于树的后续遍历。

例子:N == 10 ;原数组a:1,2,3,4,5,6,7,8,9,10;要求:区间和

 

过程如图:

 

那么问题来了,究竟要递归到多深,才到可以存放原始数据的叶节点??

这取决于:[l,r]什么时候有l==r;其实质是,取决于总结点的个数N.

这也就是为什么我们说 “线段树对于每个n的分解是唯一的,n相同的线段树,结构相同。”

 

代码:

void build(int l,int r,int root)//线段树建树
{
    if(l == r)
    {
        num[root]=1;
        return;
    }

    // [l,mid]:左子树  [mid+1,r]:右子树
    int mid = (l+r)/2;
build(l,mid,root*2);
build(mid+1,r,root*2+1);

push_up(root);
//对于这个根节点root,先把左右子树都算出来了,再来更新它的值。 //沿路回溯。回溯到的点root,都是被 [la , rb] 或其子区间影响到的点,边回溯边更新 } //调用:build(1,N,1);

 

 

 

 

 二、线段树的点修改

首先由根节点1向下递归,找到对应的叶节点,

然后,修改叶节点的值,

再向上返回,在函数返回的过程中,更新路径上的节点的统计信息。

 

 

void upgrade(int p,int val,int l,int r,int root)
//单点更新的upgrade算法:把找点p,当成找区间[p,p]
{
    if(l==r)
    {
        num[root]+=val;
        return ;
    }

    int mid = (l+r)/2;
    if(p>mid)    //如果p>当前区间的中点,说明我想找的[p,p]区间,在右半边
        upgrade(p,val,mid+1,r,root*2+1);
    else
        upgrade(p,val,l,mid,root*2);
    
    
    //沿路回溯。回溯到的点root,都是被[p,p]区间影响到的点,边回溯边更新
    num[root] = num[root*2] + num[root*2+1];
}

 

 

三、线段树的区间修改 —— 引入 lazy_tag

 

和点修改一样,也是将区间分成子区间,

首先由根节点1向下递归,找到对应的叶节点,

然后,修改本节点的值,

再向上返回,在函数返回的过程中,更新路径上的节点的统计信息。

 

由此可见向上更新的部分,是一毛一样的。

 

不同的是,点修改不用向下修改(因为对下面的没有影响,并且也没有下一级节点了,它自己就是最底层叶节点),

可是区间修改,理论上来说是需要向下修改的(因为父区间变动,子区间也会变动)

 

但是又不能,每次一区间修改,就连带着修改下面的所有子结点,这也tm太慢了。

 

所以,我们引入了 lazy_tag , 这样就可以不用把区间内的每个点都按照单点更新的方式更新一遍。

我们暂时不更新下面的子节点,而是打上一个标记,什么时候要用到子节点了,什么时候再看着这个标签,更新子节点。

(一般都是sum的时候用push_down更新子节点,因为求和的时候需要先知道两个子节点的值)

 

lazy_tag的含义:

本节点的统计信息已经根据标记更新过了,但是本节点的子节点仍需要进行更新。(注意:打上懒惰标记的节点,它自己是已经更新过了的)

 

即,如果要给一个区间的所有值都加上1,那么,实际上并没有给这个区间的所有值都加上1,而是打个标记,记下来,这个节点所包含的区间需要加1.

打上标记后,要根据标记更新本节点的统计信息,比如,如果本节点维护的是区间和,而本节点包含5个数,那么,打上+1的标记之后,要给本节点维护的和+5。

 

这是向下延迟修改,但是向上显示的信息是修改以后的信息,所以查询的时候可以得到正确的结果。

 

有的标记之间会相互影响,所以比较简单的做法是,每递归到一个区间,首先下推标记(若本节点有标记,就下推标记),然后再打上新的标记,这样仍然每个区间操作的复杂度是O(log2(n))。

 

重申,请注意:区间修改才需要懒惰标记,单点修改不需要。

 

示例代码:

void upgrade(int la,int rb,int l,int r ,int val,int root)
{
    /*以后就永远设定:
       la、rb为需更新区间的左、右端点(一直不变);
       l、r为当前区间的左、右端点,(随递归更新)
       root为当前 [l , r ] 对应的根存储位置(随递归更新)
    */

//若本次所看区间,整个就包含在所要查询的区间之内
    if(la<=l && rb>=r)
    {
        num[root] = (r-l+1)*val; //把本区间num更新为正确值
        lazy[root] = val; //增加lazy标记,表示:本区间的Sum正确,子区间的Sum仍需要根据lazy的值来调整
        return ;
    }
    
    push_down(root,r-l+1);
//在继续递归之前,先把当前root 的标记往下推
//每次都是先下推,再更新,从而保证,计算root的时候,它的左右两子树都已经是正确值,左右两子树都不存在lazy更新延迟,都已经更新好了。

    int mid = (l+r)/2;
    if(la<=mid)
    {
        upgrade(la,rb,l,mid,val,root*2);
    }
    if(rb>mid)
    {
        upgrade(la,rb,mid+1,r,val,root*2+1);
    }


    push_up(root);

}

 

四、push_up函数

我的目标是,已知两个子节点的某些信息,利用push_up函数推出父节点

思考:如果要用两个子节点推出父节点,我需要知道子节点的那些信息?————这些信息都是线段树需要维护的。

然后,已知了所有所需信息之后,具体该怎么推?————决定了:如何具体的写出 push_up函数

注意:push_up函数内,两个子节点的信息,是已经更新过的、正确的信息。因为在调用push_up之前,已经调用过push_down函数,也就是说,两个子节点的信息,是已经被更新过了的,是正确的。

 

五、push_down函数

一、push_down函数总共需要干三件事:

1:更新左右子节点的lazy值,也就是:把父节点root上面的lazy标记,下推到两个子节点上

2:依据lazy_tag的定义,更新左右子节点的num值,使左右子节点的值成为“被更新过的、正确的值”。

3:把父节点root自身的lazy清空。因为lazy_tag已经被下推,就向上查询来看,父节点自身的lazy_tag已经不需要了

二、注意:

1:开始一切动作之前,要先特判:此父节点上究竟有没有lazy_tag。如果本就没有lazy_tag,千万别更新,会wa。

2:lazy_tag的意义可以被任意定义;关于‘一’中的“更新”,具体的更新方法也是多种多样的,但关键是:

     lazy_tag的定义,要能跟 “更新num的操作”对应得上。

     也就是说,必须要能够根据lazy_tag,正确更新结点的num值才可以。

 

示例代码:

(此处lazy_tag存储的是:当前段中,每个结点被更新成的数值)

(num的意义是:此区间上的数值累加和)

(所以根据这里lazy_tag的定义,每个num 的更新方法就是:lazy的数值*区间长度)

void push_down(int root,int len)  //传入:父节点root 和 区间长度len
{ 
    if(lazy[root] == 0)  //如果此节点根本没有lazy_tag,直接返回,不作处理
        return;

    lazy[root*2] = lazy[root];       //把lazy下推到两个子节点上
    lazy[root*2+1] = lazy[root];

    num[root*2] = lazy[root*2]*(len-(len)/2);  //更新两个子节点的num
    num[root*2+1] = lazy[root*2+1]*((len)/2);、
    
    lazy[root]=0;  //清空父节点自身的lazy_tag
}

 

 

整体的示例代码:

//线段树——————区间修改,区间查询
//题意:一个线性数组,不断把某区间内的值修改“为”另一个输入值,查询区间内和。

#include <iostream>
#include <cstdio>
#include <cstring>
#define maxn 100005
int num[maxn*4];//开四倍空间
int lazy[maxn*4];
using namespace std;

void push_up(int root)//根节点状态更新
{
    num[root] = num[root*2] + num[root*2+1];
}
void build(int l,int r,int root)//线段树建树
{
    if(l == r)
    {
        num[root]=1;
        return;
    }
    int mid = (l+r)/2;
    build(l,mid,root*2);
    build(mid+1,r,root*2+1);
    push_up(root);
}

void push_down(int root,int len)//传入:root结点下标、对应的当前[l,r]区间长度
{
    if(lazy[root] == 0)//假若这个节点根本没有lazy_tag
        return;
    lazy[root*2] = lazy[root];
    lazy[root*2+1] = lazy[root];
    num[root*2] = lazy[root*2]*(len-(len)/2);
    num[root*2+1] = lazy[root*2+1]*((len)/2);

    
    lazy[root]=0;
}

void upgrade(int la,int rb,int l,int r ,int val,int root)//线段更新
//la、rb为需更新的区间左、右端点,l、r为当前区间左、右端点,root为当前l、r对应的根存储位置
{
    if(la<=l && rb>=r)
    {
        num[root] = (r-l+1)*val;
        lazy[root] = val;
        return ;
    }
    
    push_down(root,r-l+1);
    
    int mid = (l+r)/2;
    if(la<=mid)
    {
        upgrade(la,rb,l,mid,val,root*2);
    }
    if(rb>mid)
    {
        upgrade(la,rb,mid+1,r,val,root*2+1);
    }
    push_up(root);
}


int query(int la,int rb,int l,int r,int root)//查询区间[la,rb]的值
{
    if(l>=la && r<=rb)
    {
        return num[root];
    }
    
    push_down(root,r-l+1);
    
    int mid = (l+r)/2;
    int ans=0;
    if(la<=mid)
    {
        ans += query(la,rb,l,mid,root*2);
    }
    if(rb>mid)
    {
        ans +=query(la,rb,mid+1,r,root*2+1);
    }
    
    return ans;
}
int main()
{
    int t;
    scanf("%d",&t);
    for(int o=1;o<=t;o++)
    {
        int N;//原数组结点总数
        scanf("%d",&N);
        memset(num,0,sizeof(num));
        memset(lazy,0,sizeof(lazy));
        
        build(1,N,1);//建树
        int x;
        scanf("%d",&x);
        while(x--)
        {
            int la,rb,val;
            scanf("%d%d%d",&la,&rb,&val);
            
            upgrade(la,rb,1,N,val,1);//更新[l,r]区间结点,每个结点都被更新“成”val
            query(la,rb,1,N,1);//查询区间[la,rb]的值
        }
    }
    return 0;
}
View Code

 

 

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM