学习笔记——线段树

线段树

本文主要来自百度百科和这篇blog

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
长这样
在这里插入图片描述
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

基本结构与建造

线段树是建立在线段的基础上,每个结点都代表了一条线段[a,b]。长度为1的线段称为元线段。非元线段都有两个子结点,左结点代表的线段为[a,(a + b) / 2],右结点代表的线段为[((a + b) / 2)+1,b]。

长度范围为[1,L] 的一棵线段树的深度为log (L) + 1。这个显然,而且存储一棵线段树的空间复杂度为O(L)。

显然可得,节点i的权值=她的左儿子权值+她的右儿子权值,

根据这个思路,我们就可以建树了,我们设一个结构体tree,tree[i].l和tree[i].r分别表示这个点代表的线段的左右下标,tree[i].sum表示这个节点表示的线段和。

我们知道,一颗二叉树,她的左儿子和右儿子编号分别是她2和她2+1

再根据刚才的性质,得到式子: t r e e [ i ] . s u m = t r e e [ i ∗ 2 ] . s u m + t r e e [ i ∗ 2 + 1 ] . s u m tree[i].sum=tree[i*2].sum+tree[i*2+1].sum tree[i].sum=tree[i2].sum+tree[i2+1].sum;就可以建一颗线段树了

喜闻乐见抄的代码

inline void build(int i,int l,int r){//递归建树
    tree[i].l=l;tree[i].r=r;
    if(l==r){//如果这个节点是叶子节点
        tree[i].sum=input[l];
        return ;
    }
    int mid=(l+r)>>1;
    build(i*2,l,mid);//分别构造左子树和右子树
    build(i*2+1,mid+1,r);
    tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;//刚才我们发现的性质return ;
}

用 途

  1. 线段树支持最基本的操作为插入和删除一条线段。下面以插入为例,详细叙述,删除类似。
    将一条线段[a,b] 插入到代表线段[l,r]的结点p中,如果p不是元线段,那么令mid=(l+r)/2。如果b<mid,那么将线段[a,b] 也插入到p的左儿子结点中,如果a>mid,那么将线段[a,b] 也插入到p的右儿子结点中。
    插入(删除)操作的时间复杂度为O(logn)。
  2. 单点修改,区间查询
    区间查询
    一个长度为4的区间,分别是1、2、3、4,我们想求出第1~3项的和。我们要建出一颗线段树,其中点权(也就是红色)表示和。
    在这里插入图片描述
    然后我们要求1-3的和,我们先从根节点开始查询,发现她的左儿子1-2这个区间和答案区间1-3有交集,那么我们跑到左儿子这个区间。
    然后,我们发现这个区间1-2被完全包括在答案区间1-3这个区间里面,那就把她的值3返回。
    我们回到了1-4区间,发现她的右儿子3-4区间和答案区间1-3有交集,那么我们走到3-4区间
    到了3-4区间,我们发现她并没有完全包含在答案区间1-3里面,但发现她的左儿子3-3区间和1-3区间又交集,那么走到3-3区间
    到了3-3区间,发现其被答案区间完全包含,就返回她的值3一直到最开始
    3-3区间的3与1-2区间的3=6,我们知道了1-3区间和为6.
    有人可能会说你这样是不是疯了,我那脚都能算出1+2+3=6,为什么这么麻烦?!
    因为这才几个数,如果一百万个数,这样时间会大大增快。
    我们总结一下,线段树的查询方法:

1、如果这个区间被完全包括在目标区间里面,直接返回这个区间的值
2、如果这个区间的左儿子和目标区间有交集,那么搜索左儿子
3、如果这个区间的右儿子和目标区间有交集,那么搜索右儿子

喜闻乐见抄的代码

inline int search(int i,int l,int r){
    if(tree[i].l>=l && tree[i].r<=r)//如果这个区间被完全包括在目标区间里面,直接返回这个区间的值
        return tree[i].sum;
    if(tree[i].r<l || tree[i].l>r)  return 0;//如果这个区间和目标区间毫不相干,返回0
    int s=0;
    if(tree[i*2].r>=l)  s+=search(i*2,l,r);//如果这个区间的左儿子和目标区间又交集,那么搜索左儿子
    if(tree[i*2+1].l<=r)  s+=search(i*2+1,l,r);//如果这个区间的右儿子和目标区间又交集,那么搜索右儿子
    return s;
}

单点修改

然后,我们怎么修改这个区间的单点,其实这个相对简单很多,你要把区间的第dis位加上k。

那么你从根节点开始,看这个dis是在左儿子还是在右儿子,在哪往哪跑,

然后返回的时候,还是按照tree[i].sum=tree[i2].sum+tree[i2+1].sum的原则,更新所有路过的点

如果不理解,我还是画个图吧,其中深蓝色是去的路径,浅蓝色是返回的路径,回来时候红色的+标记就是把这个点加上这个值。

蒟蒻抄的图片
在这里插入图片描述

inline void add(int i,int dis,int k){
    if(tree[i].l==tree[i].r){//如果是叶子节点,那么说明找到了
        tree[i].sum+=k;
        return ;
    }
    if(dis<=tree[i*2].r)  add(i*2,dis,k);//在哪往哪跑
    else  add(i*2+1,dis,k);
    tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;//返回更新
    return ;
}
  1. 区间修改与单点查询
    区间修改

区间修改和单点查询,我们的思路就变为:如果把这个区间加上k,相当于把这个区间涂上一个k的标记,然后单点查询的时候,就从上跑道下,把沿路的标记加起来就好。

这里面给区间贴标记的方式与上面的区间查找类似,原则还是那三条,只不过第一条:如果这个区间被完全包括在目标区间里面,直接返回这个区间的值变为了如果这个区间如果这个区间被完全包括在目标区间里面,给这个区间标记k

区间修改

inline void add(int i,int l,int r,int k){
    if(tree[i].l>=l && tree[i].r<=r){//如果这个区间被完全包括在目标区间里面,讲这个区间标记k
        tree[i].sum+=k;
        return ;
    }
    if(tree[i*2].r>=l)
        add(i*2,l,r,k);
    if(tree[i*2+1].l<=r)
        add(i*2+1,l,r,k);
}

单点查询

void search(int i,int dis){
    ans+=tree[i].num;//一路加起来
    if(tree[i].l==tree[i].r)
        return ;
    if(dis<=tree[i*2].r)
        search(i*2,dis);
    if(dis>=tree[i*2+1].l)
        search(i*2+1,dis);
}

感觉到了这里初学者可以歇一歇,不然容易脑袋冒烟。

进 阶 使 用

  1. 区间修改+区间查询
    有的人会想,诶我把前面讲的综合一下不就得了,然后你发现这样是错误的。
    例如我对于一个长度为[1,4]的线段树进行区间修改,对[1,3]的区间+1,那么我们标记的是区间[1,2]与[3],但是进行[2,4]查询时查询的是没有标记的[2]与[3,4],那么之前对于2,3的修改就毫无作用导致了错误。
    那么我们应该怎么办?这时候pushdown的作用就显现出来了。
    你会想到,我们只需要在查询的时候,如果我们要查的2节点在[1,2]区间的里面,那我们就可以把[1,2]区间标记的那个+1给推下去这样就能顺利地加上了。
    怎么记录这个标记呢?我们需要记录一个“懒标记”lazytage,来记录这个区间
    介绍一下Lazy思想:对整个结点进行的操作,先在结点上做标记,而并非真正执行,直到根据查询操作的需要分成两部分。

区间修改的时候,我们按照如下原则:

1、如果当前区间被完全覆盖在目标区间里,那么把这个区间的 s u m + k ∗ ( t r e e [ i ] . r − t r e e [ i ] . l + 1 ) sum+k*(tree[i].r-tree[i].l+1) sum+k(tree[i].rtree[i].l+1)(区间长度*每个点修改值)

2、如果没有完全覆盖,则先下传懒标记

3、如果这个区间的左儿子和目标区间有交集,那么搜索左儿子

4、如果这个区间的右儿子和目标区间有交集,那么搜索右儿子

然后查询的时候,将这个懒标记下传就好了,下面图解一下:

如图,区间[1,4]分别是1、2、3、4,我们要把[1,3]区间+1。因为[1,2]区间被完全覆盖,所以将其+2,并将紫色的lazytage+1,3区间同理.我们处理完这些以后,还是要按照 t r e e [ i ] . s u m = t r e e [ i ∗ 2 ] . s u m + t r e e [ i ∗ 2 + 1 ] . s u m tree[i].sum=tree[i*2].sum+tree[i*2+1].sum tree[i].sum=tree[i2].sum+tree[i2+1].sum的原则返回在这里插入图片描述
区间修改代码Part 1

void add(int i,int l,int r,int k)
{
    if(tree[i].r<=r && tree[i].l>=l)//如果当前区间被完全覆盖在目标区间里,讲这个区间的sum+k*(tree[i].r-tree[i].l+1)
    {
        tree[i].sum+=k*(tree[i].r-tree[i].l+1);
        tree[i].lz+=k;//记录lazytage
        return ;
    }
    push_down(i);//向下传递
    if(tree[i*2].r>=l)
        add(i*2,l,r,k);
    if(tree[i*2+1].l<=r)
        add(i*2+1,l,r,k);
    tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;
    return ;
}

其中的pushdown,就是把自己的lazytage归零,并给自己的儿子加上,并让自己的儿子加上k*(r-l+1)(区间长度*每个点的修改值)
Part 2

void push_down(int i)
{
    if(tree[i].lz!=0)
    {
        tree[i*2].lz+=tree[i].lz;//左右儿子分别加上父亲的lz
        tree[i*2+1].lz+=tree[i].lz;
        init mid=(tree[i].l+tree[i].r)/2;
        tree[i*2].data+=tree[i].lz*(mid-tree[i*2].l+1);//左右分别求和加起来
        tree[i*2+1].data+=tree[i].lz*(tree[i*2+1].r-mid);
        tree[i].lz=0;//父亲lz归零
    }
    return ;
}

查询的时候,和上一章的几乎一样,就是也要像修改一样加入pushdown,这里用图模拟一下。我们要查询[2,4]区间的和,这是查询前的情况,所有紫色的代表lazytage
在这里插入图片描述

然后,我们查到区间[1,2]时,发现这个区间并没有被完全包括在目标区间里,于是我们就pushdown,lazytage下传,并让每个区间sum加上(r-l)*lazytage。在这里插入图片描述
然后查到[2,2]区间,发现被完全包含,所以就返3,再搜索到[3,4]区间,发现被完全包含,那么直接返回8,最后3+8=11就是答案

inline int search(int i,int l,int r){
    if(tree[i].l>=l && tree[i].r<=r)
        return tree[i].sum;
    if(tree[i].r<l || tree[i].l>r)  return 0;
    push_down(i);
    int s=0;
    if(tree[i*2].r>=l)  s+=search(i*2,l,r);
    if(tree[i*2+1].l<=r)  s+=search(i*2+1,l,r);
    return s;
}

好了,到了这里,我们就学会了用线段树进行区间加减操作,大家可以完成洛谷的线段树模板1.
洛谷P3372

乘法(根号)线段树

1
乘法线段树

如果这个线段树只有乘法,那么直接加入lazytage变成乘,然后tree[i].sum*=k就好了。但是,如果我们是又加又乘,那就不一样了。

当lazytage下标传递的时候,我们需要考虑,是先加再乘还是先乘再加。我们只需要对lazytage做这样一个处理。

lazytage分为两种,分别是加法的plz和乘法的mlz。

mlz很简单处理,pushdown时直接*父亲的就可以了,那么加法呢?

我们需要把原先的plz*父亲的mlz再加上父亲的plz.

inline void pushdown(long long i){//注意这种级别的数据一定要开long long
    long long k1=tree[i].mlz,k2=tree[i].plz;
    tree[i<<1].sum=(tree[i<<1].sum*k1+k2*(tree[i<<1].r-tree[i<<1].l+1))%p;//
    tree[i<<1|1].sum=(tree[i<<1|1].sum*k1+k2*(tree[i<<1|1].r-tree[i<<1|1].l+1))%p;
    tree[i<<1].mlz=(tree[i<<1].mlz*k1)%p;
    tree[i<<1|1].mlz=(tree[i<<1|1].mlz*k1)%p;
    tree[i<<1].plz=(tree[i<<1].plz*k1+k2)%p;
    tree[i<<1|1].plz=(tree[i<<1|1].plz*k1+k2)%p;
    tree[i].plz=0;
    tree[i].mlz=1;
    return ;
}

然后加法和减法的函数同理,维护lazytage的时候加法标记一定要记得现乘再加。

值得一提的是,计算*2时一定要改成i<<1这样能解决很多时间,还有要开long long,还有,函数前面要加inline 我在其他OJ交这道题时,就因为没加inline 就被卡了,交了就过了。
2、根号线段树

其实,根号线段树和除法线段树一样。她们乍眼一看感觉直接用lazytage标记除了多少,但是实际上,会出现精度问题。

c++的除法是向下取整,很明显,(a+b)/k!=a/k+b/k(在向下取整的情况下),而根号,很明显根号(a)+根号(b)!=根号(a+b)那么怎么办?

第一个想法就是暴力,对于每个要改动的区间l~r,把里面的每个点都单独除,但这样就会把时间复杂度卡得比大暴力都慢(因为多个常数),所以怎么优化?

我们对于每个区间,维护她的最大值和最小值,然后每次修改时,如果这个区间的最大值根号和最小值的根号一样,说明这个区间整体根号不会产生误差,就直接修改(除法同理)

其中,lazytage把除法当成减法,记录的是这个区间里每个元素减去的值。

下面是根号线段树的修改过程:

inline void Sqrt(int i,int l,int r){
    if(tree[i].l>=l && tree[i].r<=r && (tree[i].minn-(long long)sqrt(tree[i].minn))==(tree[i].maxx-(long long)sqrt(tree[i].maxx))){//如果这个区间的最大值最小值一样
        long long u=tree[i].minn-(long long)sqrt(tree[i].minn);//计算区间中每个元素需要减去的
        tree[i].lz+=u;
        tree[i].sum-=(tree[i].r-tree[i].l+1)*u;
        tree[i].minn-=u;
        tree[i].maxx-=u;
            //cout<<"i"<<i<<" "<<tree[i].sum<<endl;
        return ;
    }
    if(tree[i].r<l || tree[i].l>r)  return ;
    push_down(i);
    if(tree[i*2].r>=l)  Sqrt(i*2,l,r);
    if(tree[i*2+1].l<=r)  Sqrt(i*2+1,l,r);
    tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;
    tree[i].minn=min(tree[i*2].minn,tree[i*2+1].minn);//维护最大值和最小值
    tree[i].maxx=max(tree[i*2].maxx,tree[i*2+1].maxx);
    //cout<<"i"<<i<<" "<<tree[i].sum<<endl;
    return ;
}

然后pushdown没什么变化,就是要记得tree[i].minn、tree[i].maxx也要记得-lazytage。
这样就可以做第二道模板
LuoguP3373

关于Pushdown

Pushdown(来自这里)
先声明一下,Pushdown这个操作基本上只有在区间修改上产生作用,只要我们是区间修改,我们不可能单个单个点的改,这样的话,复杂度比写暴力还大!!!!所以说我们的想法就是在我们找到的要改的一整个区间上加上一个标记,Lazytag,然后在后续我们要查找比该区间小的区间的时候我们顺势把Lazytag下传下去,这样我们就可以达到目的了。而Pushdown正好就是干这件事情的。

那么我们Pushdown的时候要注意一些什么呢?

首先我们要注意的就是不能引起下传混乱,比如说我们一个节点被打上了标记并被修改了,然后后续再Pushdown的时候,该标记因为还在该节点上,然后该节点就又被修改了一次,然后标记才被下传。
为了避免上面这个情况,我们约定我们的Lazytag标记是给该节点的儿子们看的,而该节点不用管,因为,我们又规定,每次当我们modify的时候找到了我们要修改的区间,那么我们就直接更改当前区间的信息,然后再为改区间打上标记。

所以说有了上面的这些约定,我们在Pushdown中干的事情也就非常的清晰并且不混乱了!!!我们每次只需要下传当前父亲节点的标记,然后修改儿子所在的当前区间即可!!

P.s:修改区间函数可以重新写一个add函数,这样可以省去非常多的代码,并且使整篇代码显得非常的清晰。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值