线段树
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
以上内容摘抄自百度百科.
一般来说线段树长这样(画的有点丑),每一个节点(非叶节点)都是从它的两个子树的根节点合并而成,所以线段树维护的值必须支持合并(具体下面会讲到),对于每一次的修改可以直接修改若干颗子树,可以证明最多修改的子树不会超过
棵,所以每次的修改的时间复杂度为
,查询同理.
线段树的作用
可以在logN的复杂度内修改和查询一定的信息(如区间加一个数,查询区间和,查询区间最大值等).
线段树常用来维护DP,甚至可以使暴力获得更多分.(骗分必备)
线段树模板的代码(部分)
- define
#define Lson now*2;
#define Rson now*2+1
#define Middle (left+right)/2
#define Left Lson,left,Middle
#define Right Rson,Middle+1,right
#define Now nowleft,nowright
先从最基础的开始例题1
- PushUp
线段树最先要写的部分是合并(应该,个人习惯)
可以发现这里要维护的是一个区间和,那么合并就很简单了
void PushUp(int now)
{
tree[now].sum=tree[Lson].sum+tree[Rson].sum;
}
- Build
写完合并,接下来要建树(有些题目可能用不上)
void Build(int now=1,int left=1,int right=N)//建树的初始值固定
{
if(left==right)//叶节点时直接赋值
{
tree[now].sum=arr[left];
return;
}
Build(Left);//建左子树
Build(Right);//建右子树
PushUp(now);//建完后需要合并
}
- Lazy标记&PushDown
下面,就要引出线段树的精髓了:Lazy标记,又称懒标记,如果没有这个标记在每次修改时的时间复杂度会变得很高,没法达到只修改 个值.
如图,需要修改蓝色区域的值,那么它覆盖的部分为红色的两颗子树,但是绿色的位置的值也发生了改变,这时就需要在红色位置打上lazy标记,lazy标记也要支持合并,在以后的修改时需要查询到绿色部分时才会将标记下推.
如图,需要查询紫色部分的值,那么如果需要查询蓝色部分的那两颗子树的值,这时就需要将红色位置的标记下推,为了得到蓝色部分的真实的值.
下面需要写推懒标记的部分:
void Down(int now,int left,int right,int lazy_)//修改子树
{
tree[now].sum+=(right-left+1)*lazy_;//子树表示的值需要加上子树长度*每个数增加的值
lazy[now]+=lazy_;//子树懒标记增加
}
void PushDown(int now,int left,int right)
{
Down(Left,lazy[now]);//修改左子树
Down(Right,lazy[now]);//修改右子树
lazy[now]=0;//lazy标记必须清空
}
- UpData
写下来就是修改部分了,这点并没有什么特别值得再说的地方.
void UpData(int nowleft,int nowright,int num,int now=1,int left=1,int right=N)
//其中的now,left,right一般不会变,所以就缺省了
{
if(left>nowright||nowleft>right)return;//如果需要查询的区间与当前区间没有公共位置则推出
if(nowleft<=left&&right<=nowright)//如果需要查询的区间包含了当前区间可以直接修改
{
tree[now].sum+=num*(right-left+1);
lazy[now]+=num;//注意修改懒标记
return;
}
PushDown(now,left,right);//需要推一下懒标记为了下方的修改
UpData(Now,num,Left);//修改左子树
UpData(Now,num,Right);//修改右子树
PushUp(now);//合并
}
- Query
查询与修改类似.
int Query(int nowleft,int nowright,int now=1,int left=1,int right=N)
{
if(left>nowright||nowleft>right)return 0;//不在范围内
if(nowleft<=left&&right<=nowright)//包含了
{
return tree[now].sum;
}
PushDown(now,left,right);//推懒标记
int result=0;
result+=Query(Now,Left);//需要将左右子树的值相加
result+=Query(Now,Right);
PushUp(now);//合并后再退出
return result;
}
线段树模板的代码(完整)
#include<bits/stdc++.h>
#define rap(i,first,last) for(int i=first;i<=last;++i)//就是那个rap蛤
#define Lson (now<<1)
#define Rson (now<<1|1)
#define Middle ((left+right)>>1)
#define Left Lson,left,Middle
#define Right Rson,Middle+1,right
#define Now nowleft,nowright
using namespace std;
const int maxN=1e5+7;
int N,M;
struct Tree
{
long long sum;
}tree[maxN*4];
long long arr[maxN];
int lazy[maxN*4];
void PushUp(int now)
{
tree[now].sum=tree[Lson].sum+tree[Rson].sum;
}
void Build(int now=1,int left=1,int right=N)
{
lazy[now]=0;
if(left==right)
{
tree[now].sum=arr[left];
return;
}
Build(Left);
Build(Right);
PushUp(now);
}
void Down(int now,int left,int right,int lazy_)
{
tree[now].sum+=(right-left+1)*lazy_;
lazy[now]+=lazy_;
}
void PushDown(int now,int left,int right)
{
if(!lazy[now])return;
Down(Left,lazy[now]);
Down(Right,lazy[now]);
lazy[now]=0;
}
void UpData(int nowleft,int nowright,int num,int now=1,int left=1,int right=N)
{
if(left>nowright||nowleft>right)return;
if(nowleft<=left&&right<=nowright)
{
tree[now].sum+=num*(right-left+1);
lazy[now]+=num;
return;
}
PushDown(now,left,right);
UpData(Now,num,Left);
UpData(Now,num,Right);
PushUp(now);
}
long long Query(int nowleft,int nowright,int now=1,int left=1,int right=N)
{
if(left>nowright||nowleft>right)return 0;
if(nowleft<=left&&right<=nowright)
{
return tree[now].sum;
}
PushDown(now,left,right);
long long result=Query(Now,Left)+Query(Now,Right);
PushUp(now);
return result;
}
int main()
{
scanf("%d%d",&N,&M);
rap(i,1,N)scanf("%lld",&arr[i]);
Build();
int check,left,right,num;
rap(i,1,M)
{
scanf("%d%d%d",&check,&left,&right);
if(check==1)
{
scanf("%d",&num);
UpData(left,right,num);
}
if(check==2)
{
printf("%lld\n",Query(left,right));
}
}
return 0;
}
推荐几道比较好的题:
模板:
线段树2
最大数
练习:
无聊的数列
方差
贪婪大陆
好一个一中腰鼓!(注意要用线段树,虽然暴力可以过)
加强:
序列维护
CPU监控
权值线段树
权值线段树其实很简单,类似一个桶,每次修改都是单点修改,所以连lazy标记都不用
权值线段树的的作用
可以很容易得出整个数列第k大值,只需要在树上二分就行了.
也可以处理一些区间中出现次数最多的数的个数之类的问题.
例题
权值线段树的代码(部分)
- UpData
void UpData(int num,int now=1,int left=1,int right=N)
{
if(num>right||num<left)return;//按这个数的大小左右二分,最多LogN次就到叶节点,所以每次修改的时间复杂度为O(LogN)
tree[now]++;//可以直接修改
if(left==right)
{
return;
}
UpData(num,Left);
UpData(num,Right);
}
- Query
int Query(int num,int now=1,int left=1,int right=N)
{
if(num==0)return 0;//可有可无
if(left==right)//到叶节点说明找到了
return left;
if(tree[Lson]>=num)return Query(num,Left);//如果在左子树就往左子树找
else return Query(num-tree[Lson],Right);//不在就往右子树找
}
有一点需要注意的是这类题中的数往往较大,但个数一般不会超过5e5,所以需要用到离散化.
练习:
三元上升子序列
普通平衡树(你没有看错)
动态开点
动态开点用在一些数据特别大(主要是权值线段树)和线段树合并(我现在还不会)中.
动态开点的作用&原理
在一些用到权值线段树的题目中数据如果数据大于1e6基本就会跑不过了,但是,有了动态开点,可以节省很多用不到的点.
如图,在这样一颗线段树中,灰色部分的节点时没有用的如果在权值线段树中像这样灰色的节点的数量可能会很多,这样极大的浪费了空间,所以需要用一种新的方式来存这棵树,动态开点就是这样出现的.
在原本的线段树数中一个节点x的左儿子为x*2右儿子为x*2+1.
而现在,不能通过计算的方式获得儿子的位置,需要再开两个数组来记录左右儿子的位置,每次修改时如果需要修改到一个没有加入的节点时才会将这个节点放入这颗树中,这样可以节省很多的空间.
动态开点的代码(部分)
大体与普通线段树类似,但是在带修改的部分时传入的now为地址需要修改.
一下内容为线段树1动态开点的部分代码
//以下部分需要更改
#define Lson tree[now].lson
#define Rson tree[now].rson
struct Tree
{
int lson,rson;
long long sum;
}
- PushDown
void Down(int &now/*在Down时会修改部分的值,所以需要传入地址*/,int left,int right,int add)
{
if(!now)now=++cnt;//如果没有这个点就加入这个点
tree[now].sum+=(right-left+1)*add;
lazy[now]+=add;
}
void PushDown(int now,int left,int right)
{
if(lazy[now])//如果不加在根本没有需要下传的标记时可能会多开很多的点
{
Down(Left,lazy[now]);
Down(Right,lazy[now]);
lazy[now]=0;
}
}
- UpData
void UpData(int nowleft,int nowright,int add,int &now/*这里也需要传入一个地址*/,int left=1,int right=N)
{
if(nowleft>right||left>nowright)return;
if(!now)now=++cnt;//如果没有这个点就加入这个点
if(nowleft<=left&&right<=nowright)
{
tree[now].sum+=(right-left+1)*add;
lazy[now]+=add;
return;
}
PushDown(now,left,right);
UpData(Now,add,Left);
UpData(Now,add,Right);
PushUp(now);
}
mian()
int root=1;//其实root并不会被修改,但是因为需要传入一个变量所以必须要有这样一个量
int mian()
{
scanf("%d%d",&N,&M);
cnt=1;
rap(i,1,N)
{
scanf("%d",&arr[i]);
UpData(i,i,arr[i],root);//直接加入这棵树
}
int check,L,R,X;
rap(i,1,M)
{
scanf("%d%d%d",&check,&L,&R);
if(check==1)
{
scanf("%d",&X);
UpData(L,R,X,root);//这里也需要传入root
}
else
printf("%lld\n",Query(L,R));//Query没有什么区别.
}
}
线段树合并
线段树合并基于动态开点,所以需要先学习有关动态开点的内容后再看一下这部分内容
在一些题目中需要将两颗线段树和在一起,组成一棵更大的线段树,这时就需要用到线段树合并.
在如图所示的两颗线段树中(线段树为动态开点,灰色部分为没有值),如果把这两颗线段树合并后为:
在有值的位置将两颗线段树的值相加,如果有一棵线段树这一个位置没有值则为另一棵线段树的值.
线段树合并的作用
可以将两颗线段树和为一棵线段树,看起来可能没什么用,先拿一道例题康康,查询第k大值,这就很容易想到权值线段树了,但是,将不同的岛相连后这两个岛可以说变为一个岛,这时,线段树合并就派上用场了,可以对于每个岛开一棵权值线段树,在不同的岛相连时将线段树合并,用并查集判断连通性和每一片岛中的root,这样就可以很轻松的写出这道题了.
线段树合并的代码(部分)
主要部分与权值线段树+动态开点类似.
- Merge
以下代码为将tree2合并到tree1上.
void Merge(int &tree1/*因为tree1是要修改的,所以需要传入地址*/,int tree2,int left=1,int right=N)
{
if(!tree1||!tree2)//当当前子树在tree1和tree2中有一个没有时直接就是有的那棵
{
tree1+=tree2;
return;
}
if(left==right)//叶节点时tree1不变,将叶节点的值相加
{
tree[tree1].sum=tree[tree1].sum+tree[tree2].sum;
return;
}
//向左右合并这颗线段树
Merge(tree[tree1].lson,tree[tree2].lson,left,Middle);
Merge(tree[tree1].rson,tree[tree2].rson,Middle+1,right);
PushUp(tree1);//将合并后的线段树上的值合并
}
线段树合并的复杂度
可以发现线段树合并时如果两颗树都有的部分需要全部扫一遍,有一棵树没有的部分可以直接返回,所以它的复杂度为两树在同一位置都有节点的节点的个数.
线段树空间回收
可以发现在线段树合并中会浪费掉很多的空间(每次合并节点时就会有一个节点被浪费掉),在比赛中每一点的空间都是极为珍贵,于是就出现了线段树的空间回收.
空间回收的作用&原理
可以将一些不再会有用的点先存放起来,在未来需要开出一个新的点时可以先将这些被扔掉的点用掉,在线段树合并中可以节省大量的空间.
空间回收的原理也非常简单,一个
数组,对于每一次浪费掉的节点放入这个数组中,如果需要开一个新的节点时可以看一下这个数组中有没有节点,有就用掉,没有就再开一个新的节点,这样可以节省大量的空间.
空间回收的代码(部分)
- Del
删除一个点
void Del(int now)
{
tree[now].lson=tree[now].rson=0;
tree[now]./*这个节点中的内容都要删除*/;
rubbish[++tot]=now;//可以将这个rubbish数组理解为一个栈,tot为栈顶
}
- New
建一个新的节点.
int New()
{
if(tot)return rubbish[tot--];//弹出栈顶元素
return ++cnt;
}//代码非常的简单,但是有着巨大的作用
空间回收只能用在一些特殊的线段树中(主要是线段树合并).
线段树分裂
这是一个没有什么用的东西
找了很久也没有什么特别好的题和讲解的博客,把找到的唯一一道题扔出来.
这道题因为可以离线做所以难度不大,但是用线段树分裂合并以及set维护可以做到
.
如图这样的一棵线段树,需要分裂出其中橙色的部分,需要新建几个节点(绿色),需要把原来的数中与这颗子树有关的边断开(红色线段树分开的边).
线段树分裂的代码(部分)
- Split
void Split(int &tree1,int &tree2,int nowleft,int nowright,int left=1,int right=N)
//在tree1这棵权值线段树中把left~right的部分分裂到tree2中
{
if(right<nowleft||nowright<left)return;//不在区间内
if(!tree1)return;//如果tree1没有那自然没有用了
if(nowleft<=left&&right<=nowright)//如果在范围内就直接赋值
{
tree2=tree1;//直接连到tree2中
tree1=0;//如果当前区间已经被完全覆盖了就需要把这条边断开
return;
}
if(left==right)return;//叶节点返回
if(!tree2)tree2=New();//如果不在范围内需要开一个新的点(绿色部分)
//左右区间分裂
Split(tree[tree1].lson,tree[tree2].lson,Now,left,Middle);
Split(tree[tree1].rson,tree[tree2].rson,Now,Middle+1,right);
PushUp(tree1);//最后合并信息
PushUp(tree2);
}
线段树分裂的时间复杂度
最多只会断开 条边,所以时间复杂度是 .
标记永久化
普通的线段树需要PushUp和PushDown,那有没有存在一种线段树在修改时不用到他们呢,标记永久化就出现了.
标记永久化的作用&原理
可以缩短代码,再也不用写PushUp和PushDown了,主要用在主席树这种不适合修改的数据结构上.
对于每次修改就在修改的最上层的点上打上标记,在查询时就只需要一路向下将标记合并就行了.
标记永久化的代码(部分)
- 标记
struct Tree
{
long long sum;
long long tag;//需要加上一个标记用的量,但是不用懒标记
}tree[maxN*4];
- PushDown
它死了 - UpData
void UpData(int nowleft,int nowright,int add,int now=1,int left=1,int right=N)
{
if(nowright<left||right<nowleft)return;
tree[now].sum+=1ll*(min(nowright,right)-max(nowleft,left)+1)*add;
//修改的区间对于当前区间的贡献
if(nowleft<=left&&right<=nowright)
{
tree[now].tag+=add;//包含了就在tag中加上
return;
}
UpData(Now,add,Left);
UpData(Now,add,Right);
//PushUp好像也没有什么必要了
}
- Query
long long Query(int nowleft,int nowright,int now=1,int left=1,int right=N)
{
if(nowright<left||right<nowleft)return 0;
if(nowleft<=left&&right<=nowright)return tree[now].sum;//包含就直接返回
return 1ll*(min(nowright,right)-max(nowleft,left)+1)*tree[now].tag//当前的tag对于这次查询的贡献
+Query(Now,Left)+Query(Now,Right);//左右区间的值
}
二维线段树
二维线段树有两种写法,四分树和树套树,根据dalao的说法,四分树非常的菜很容易被卡,最坏时修改需要O(N),所以以下内容主要讲树套树的写法.
二维线段树的作用&原理
顾名思义,用来修改二维平面上的东西(用处其实不大),这里的树套树是在线段树的每个节点上再开一棵线段树,这样每次修改和查询的时间复杂度就是
(大概).
二维线段树的代码(部分)
拿出一道模板题.
区间最大值+区间覆盖(但是这里的区间是一个矩形),对于线段树其实没什么变化,就是要用上标记永久化,据说二维线段树不支持打标记.
- 内层的线段树
struct SegmentTreeX//用两个结构体表示外传的线段树和内层的线段树,这样方便一点
{
int tree[maxN<<2],tag[maxN<<2]/*用于记录标记*/;
void UpData(int nowleft,int nowright,int cover,int now=1,int left=1,int right=M)//这里就是最普通的线段树了
{
if(nowright<left||right<nowleft)return;
tree[now]=max(tree[now],cover);//修改当前子树
if(nowleft<=left&&right<=nowright)
{
tag[now]=max(tag[now],cover);///修改标记
return;
}
UpData(Now,cover,Left);
UpData(Now,cover,Right);
}
int Query(int nowleft,int nowright,int now=1,int left=1,int right=M)//查询,实在没什么好说的
{
if(nowright<left||right<nowleft)return -INF;
if(nowleft<=left&&right<=nowright)
return max(tree[now],tag[now]);
return max(tag[now],//要和标记取max
max(Query(Now,Left),Query(Now,Right)));
}
};
- 外层的线段树
struct SegmentTreeY//外层线段树
{
SegmentTreeX tree[maxN<<2],tag[maxN<<2];//这里的每个点和标记也是一颗线段树
void UpData(int nowlx,int nowly,int nowleft,int nowright,int cover,int now=1,int left=1,int right=N)//其余基本相同,就是在原先的标记修改和子树修改需要改成修改内层的线段树
{
if(nowright<left||right<nowleft)return;
tree[now].UpData(nowlx,nowly,cover);
if(nowleft<=left&&right<=nowright)
{
tag[now].UpData(nowlx,nowly,cover);
return;
}
UpData(NowX,cover,Left);
UpData(NowX,cover,Right);
}
int Query(int nowlx,int nowly,int nowleft,int nowright,int now=1,int left=1,int right=N)//查询也差不多
{
if(nowright<left||right<nowleft)return -INF;
if(nowleft<=left&&right<=nowright)
return tree[now].Query(nowlx,nowly);
return max(tag[now].Query(nowlx,nowly),
max(Query(NowX,Left),Query(NowX,Right)));
}
}SegmentTree;
扫描线
放一道模板题.
因为的比较得菜,所以也就不放那么多图了,如果需要可以点击luogu中的题解,里面的dalao随便一个就可以吊打我,这边就放一个代码,算是也讲过这块内容了.
扫描线的代码(完整)
#include<bits/stdc++.h>//因为作者太懒,放上代码就跑路了,留下孤独的您们自行理解吧.
#define rap(i,first,last) for(int i=first;i<=last;++i)
#define Lson (now<<1)
#define Rson (now<<1|1)
#define Middle ((left+right)>>1)
#define Left Lson,left,Middle
#define Right Rson,Middle+1,right
#define L tree[now].left
#define R tree[now].right
#define Now nowleft,nowright
using namespace std;
const int maxN=5e5+7;
int N,tot;
struct Line
{
long long left,right,high;
int tag;
void into(long long left_,long long right_,long long high_,int tag_)
{
left=left_;
right=right_;
high=high_;
tag=tag_;
}
}line[maxN*2];
bool cmp(Line a,Line b)
{
return a.high<b.high;
}
struct Tree
{
int left,right,sum;
long long len;
}tree[maxN*4];
int lazy[maxN*4];
long long hashline[maxN*2];
void PushUp(int now)
{
if(tree[now].sum)
{
tree[now].len=hashline[R+1]-hashline[L];
}
else
{
tree[now].len=tree[Lson].len+tree[Rson].len;
}
}
void Build(int now=1,int left=1,int right=tot)
{
tree[now].left=left;
tree[now].right=right;
tree[now].sum=tree[now].len=0;
if(left==right)
{
return;
}
Build(Left);
Build(Right);
}
void UpData(int nowleft,int nowright,int tag,int now=1,int left=1,int right=tot)
{
if(hashline[right+1]<=nowleft||nowright<=hashline[left])return;
if(nowleft<=hashline[left]&&hashline[right+1]<=nowright)
{
tree[now].sum+=tag;
PushUp(now);
return;
}
UpData(Now,tag,Left);
UpData(Now,tag,Right);
PushUp(now);
}
int main()
{
scanf("%d",&N);
long long x1,y1,x2,y2;
rap(i,1,N)
{
scanf("%lld%lld%lld%lld",&x1,&y1,&x2,&y2);
hashline[2*i-1]=x1;
hashline[2*i]=x2;
line[2*i-1].into(x1,x2,y1,1);
line[2*i].into(x1,x2,y2,-1);
}
sort(line+1,line+1+N*2,cmp);
sort(hashline+1,hashline+1+N*2);
tot=unique(hashline+1,hashline+N*2+1)-hashline-2;
Build();
long long answer=0;
rap(i,1,N*2-1)
{
UpData(line[i].left,line[i].right,line[i].tag);
answer+=tree[1].len*(line[i+1].high-line[i].high);
}
printf("%lld",answer);
return 0;
}
以下内容先咕着
主席树
To be continued