我与线段树的故事(纯新手请进)

作为一名OI新手,能在这个高手云集的博客领域发表一篇自己的文章,我感到十分荣幸。
今天,我就给大奖讲一讲我与线段树的故事。话说“线段树”我还是上个星期才学会的。


1.什么是线段树

先进一下百度给出的定义:

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩。

好,请忽视上面引用的内容!
线段树是一种可以维护区间操作的二叉树,而它的外延则是一个序列。在线段树中,每一个结点与一个闭区间相对应,其中树的根节点表示整个区间。它的左子则代表这个区间的左半部分,它的右子代表这个区间的右半部分。每一个结点可以有一个属性值,表示所对应区间的一个属性(例如:区间最大值、区间最小值、区间和等等)。
因此,我们可以定义这样的一个结构体,用来表示线段树的结点。

struct NODE
{
    int l,r;//用于记录该节点所表示的区间
    int lch,rch;//用于记录结点的左子和右子
    int m;//区间的属性值
    int lazy;//"lazy标记" 也就是 "懒标记",后面会具体地讲它的用途
    void clr(int L,int R,int LCH,int RCH,int m)
    {
        l=L;r=R;lch=LCH;rch=RCH;m=M;
    }//这是一种供懒人使用的初始化结点的方法,当然,也可以不这么写
};

2.线段树的构建

先进代码(我自己的风格,与标准版不太一致,但是也是可以实现的)。
以维护一个“区间和”为例(struct NODE的定义参见上文):

NODE ns[NODEMAX];//定义结点们~
int newnode=1;//表示下一个等待使用的结点的编号
int array[LENMAX];//把线段树所对应的外延序列在建树之前存在这个数组里
void build(int root,int l,int r)
{//表示以编号为“root”的结点为根,建立一个表示区间[l,r]的一棵子树
    if(l==r)//说明已经确定到了序列中的一个元素,而不再是一个区间
    {
        ns[root].clr(l,r,-1,-1,array[l]);//-1是用来占位的,表示没有儿子
        //因为只有一个元素,所以它的区间和就是序列中对应位置的元素
        return;
    }
    int nl=newnode++;
    int nr=newnode++;//为左子和右子各自新拿出来一个结点编号
    int mid=(l+r)/2;//二分这个区间
    build(nl,l,mid);
    build(nr,mid+1,r);//递归地建立左子树和右子树
    ns[root].clr(l,r,nl,nr,ns[nl].m+ns[nr].m);
    //整个区间的区间和就是 左半区间的区间和 与 右半区间的区间和 的和
}

这样你就可以建立了一棵线段树了。主程序中要这样写:

int main()
{
    cin>>N;//N表示区间的结束
    ...//此处省略对array的输入

    int root=newnode++;//申请一个结点作为整棵树的根节点
    build(root,1,N);//建树

    ...//此处省略后文对线段树的维护
    return 0;
}

再比如说,要求维护区间最大值。其他部分都是一样的,“build”函数最后一句改成这个:

    ns[root].clr(l,r,nl,nr,max(ns[nl].m,ns[nr].m));

3.不带lazy单点修改

也以维护区间和为例子:

void updata(int root,int pos,int Numto)
{//表示在以编号root的结点为根的树中把 第pos个位置 的数值 修改为Numto
    if(ns[root].l==ns[root].r)//如果已经确定到了一个元素,直接修改
        ns[root].m=Numto;
    int nl=ns[root].lch;
    int nr=ns[root].rch;//左子,右子
    int mid=(ns[root].l+ns[root].r)/2;
    if(pos<=mid)//如果在左半个区间
        updata(nl,pos,Numto);//在左子树中递归修改
    else//否则
        updata(nr,pos,Numto);//在右子树中递归修改
    ns[root].m=ns[nl].m+ns[nr].m;//最后不要忘了修改当前结点的值
    //因为当这个区间中的一个元素发生改变时,整个区间的区间和一定也会发生改变
}

但是,这只是不带lazy的单点修改。如果带lazy情况就会复杂得多。


4.一个叫做lazy的懒标记

百度中关于lazy的思想是这么介绍的

(线段树)最简单的应用就是记录线段是否被覆盖,并随时查询当前被覆盖线段的总长度。那么此时可以在结点结构中加入一个变量int count;代表当前结点代表的子树中被覆盖的线段长度和。这样就要在插入(删除)当中维护这个count值,于是当前的覆盖总值就是根节点的count值了。

另外也可以将count换成bool cover;支持查找一个结点或线段是否被覆盖。

实际上,通过在结点上记录不同的数据,线段树还可以完成很多不同的任务。例如,如果每次插入操作是在一条线段上每个位置均加k,而查询操作是计算一条线段上的总和,那么在结点上需要记录的值为sum。

这里会遇到一个问题:为了使所有sum值都保持正确,每一次插入操作可能要更新O(N)个sum值,从而使时间复杂度退化为O(N)。

解决方案是Lazy思想:对整个结点进行的操作,先在结点上做标记,而并非真正执行,直到根据查询操作的需要分成两部分。

根据Lazy思想,我们可以在不代表原线段的结点上增加一个值toadd,即为对这个结点,留待以后执行的插入操作k值的总和。对整个结点插入时,只更新sum和toadd值而不向下进行,这样时间复杂度可证明为O(logN)。

对一个toadd值为0的结点整个进行查询时,直接返回存储在其中的sum值;而若对toadd不为0的一部分进行查询,则要更新其左右子结点的sum值,然后把toadd值传递下去,再对这个查询本身,左右子结点分别递归下去。时间复杂度也是O(nlogN)。

看不懂的请忽略上文内容。

lazy就是一个名字,说实话我也不知道这个名字具体有什么意义(延迟?)。但是lazy在线段树中起到了至关重要的作用,因为它可以用来做区间修改操作。就比如说有这么一道题:

给你一个整数N 和一个长度为N的序列A(A[i]表示其中第i个元素1<=i<=N),然后定义两种操作: “Q x y” 表示询问当前闭区间[x,y]的区间和,”I x y”表示把区间[x,y]中的每一个数值都加1。

首先,如果整个序列的长度是10^6,询问有10^6个,暴力维护一定会超时,这种情况下我们就要用lazy。如果我要给一个区间中的所有数都加1,我们可以把整个区间打上一个标记表示这个区间中的每个数都要加1,但是不立刻在区间上进行修改。这个时候 当前区间和=原区间和+区间元素个数*1。
请看代码:

void add(int root,int x,int y)
{
    if(ns[root].l>y || ns[root].r<x)//如果当前区间与[x,y]没有交集
        return;//直接返回
    if(x<=ns[root].l && ns[root].r<=y)//如果当前区间为[x,y]的子集
    {
        ns[root].lazy++;//直接把lazy加一
        //这里的lazy表示的就是将要给区间中每一个元素加上的数值
        //考虑到每个元素可能会被加很多次,所以不能写成"lazy=1"
        ns[root].m+=ns[root].r-ns[root].l+1;
    }
    int nl=ns[root].lch;
    int nr=ns[root].rch;//左、右子编号
    if(ns[root].lazy!=0)//如果当前结点有状态
    {
        ns[nl].lazy=ns[root].lazy;
        ns[nr].lazy=ns[root].lazy;//把这个结点的lazy状态传给子树
        ns[nl].m+=(ns[nl].r-ns[nl].l+1)*ns[root].lazy;
        ns[nr].m+=(ns[nr].r-ns[nr].l+1)*ns[root].lazy;//修改子树的属性值
        ns[root].lazy=0;//取消当前结点的状态
    }
    add(nl,x,y);
    add(nr,x,y);//递归分析子树
    ns[root].m=ns[nl].m+ns[nr].m;//这句话一定要记得写上
}

上文的这个代码可能会与常规的代码不同,但是理论上无可厚非。这里的add函数表示把当前树所表示的区间与[x,y]的交集中的每个元素加一。当你的询问区间不能恰好覆盖一个结点时…
比如有一个四个数的序列[1,4],建树之后就会有7个节点分别代表区间[1,4] (root),[1,2],[1,1],[2,2],[3,4],[3,3],[3,4]。
我要修改区间[1,3],程序就会把区间[1,2]和区间[3,3]分别打上lazy,而这个功能是在递归的过程中实现的。

但是,在更改区间部分的状态时,一定要想到把这个区间原有的状态传给它的子节点。当我把[2,2]加一时,原来的区间[1,2]已经有一个lazy=1。直接在这个lazy上加一,区间[1,1]就会非常不服,因为你凭空多给他也加了个一;要是不管这个lazy,区间[2,2]就不能算出正确的m属性值。所以最好的办法就是把这个标记下传给他的两个子结点。给整个区间+1与给它的左半区间和右半区间分别+1是等价的,然后再去处理给[2,2]加一的操作,就能得到正确的答案。(另外,在状态向下传的时候,一定要考虑到子结点m值的变化,以保证我的m值一直都是正确的)

上面的一段话可以总结为:当一个结点所表示的区间的状态的连续性被破坏时,就要把原来的状态下传。

接下来是查询的代码,表示当前树所表示的区间与[x,y]的交集中每个元素的和:

int sum(int root,int x,int y)
{
    if(ns[root].l>y || ns[root].r<x)//没有交集返回0
        return 0;
    if(x<=ns[root].l && ns[root].r<=y)//完全属于直接返回属性值
        return ns[root].m;
    int nl=ns[root].lch;
    int nr=ns[root].rch;
    if(ns[root].lazy!=0)//有状态要下传
    {
        ns[nl].lazy=ns[root].lazy;
        ns[nr].lazy=ns[root].lazy;//把这个结点的lazy状态传给子树
        ns[nl].m+=(ns[nl].r-ns[nl].l+1)*ns[root].lazy;
        ns[nr].m+=(ns[nr].r-ns[nr].l+1)*ns[root].lazy;//修改子树的属性值
        ns[root].lazy=0;//取消当前结点的状态
    }
    return sum(nl,x,y)+sum(nr,x,y);//递归处理左右子
}

同理,在带lazy做点的修改updata时,也要考虑到状态下传的问题。


5.总结

线段树可以用来解决 动态区间修改与查询 问题,区间修改也可以是多种多样的(不仅仅是加上一个数,再比如:把一个区间中的所有数变为“0”、把一个区间中的所有数变为“1”等等)。同一个线段树中还可能存在多种“lazy”(比如:lazyAdd表示在每个数身上加一个数、lazyMul表示在每个数身上乘一个数 等等不同种类的“lazy”是可以同时存在的,想想如何实现吧!)

新手上路,请多关照。如果有写错的地方,希望各位同学谅解~。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值