线段树是什么
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
以上内容摘抄自百度百科(虽然这句话并没有讲到什么,只是知道了线段树NB)
一般来说线段树长这样(画的有点丑),每一个节点(非叶节点)都是从它的两个子树的根节点合并而成,所以线段树维护的值必须支持合并(具体下面会讲到),对于每一次的修改可以直接修改若干颗子树,可以证明最多修改的子树不会超过LogN棵,所以每次的修改的时间复杂度为LogN,查询同理.
线段树怎么写
使用的代码中包含define
//以下为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标记
下面,就要引出线段树的精髓了:Lazy标记,又称懒标记,如果没有这个标记在每次修改时的时间复杂度会变得很高,没法达到只修改LogN个值.
如图,需要修改蓝色区域的值,那么它覆盖的部分为红色的两颗子树,但是绿色的位置的值也发生了改变,这时就需要在红色位置打上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);//合并
}
查询与修改类似.
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*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
using namespace std;
const int maxN=1e5+7;
int N,M;
struct Tree//目前需要维护的值只有区间和
{
int sum;
}tree[maxN*4];
int 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)
{
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)
{
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);
}
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;
}
int main()
{
scanf("%d%d",&N,&M);
rap(i,1,N)scanf("%d",&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("%d\n",Query(left,right));
}
}
return 0;
}
推荐几道比较好的题:
模板:
线段树2
最大数
训练:
无聊的数列
方差
贪婪大陆
好一个一中腰鼓!(注意要用线段树,虽然暴力可以过)
加强:
序列维护
CPU监控
权值线段树
权值线段树其实很简单,类似一个桶,每次修改都是单点修改,所以连lazy标记都不用
权值线段树的的作用
可以很容易得出整个数列第k大值,只需要在树上二分就行了.
例题权值线段树的写法
这个不用建树,合并也只有相加也就不讲了.
修改部分:
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);
}
查询:
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,所以需要用到离散化.
上一个完整的代码:
#include<bits/stdc++.h>
#define rap(i,first,last) for(int i=first;i<=last;++i)
#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
using namespace std;
int N,M;
const int maxN=1e5+7;
int tree[maxN*4];
void PushUp(int now)
{
tree[now]=tree[Lson]+tree[Rson];
}
void UpData(int num,int now=1,int left=1,int right=N)
{
if(num>right||num<left)return;
tree[now]++;
if(left==right)
{
return;
}
UpData(num,Left);
UpData(num,Right);
}
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);
}
map<int,int>Hash;
int arr[maxN];
int sor[maxN];
int num[maxN];
int main()
{
scanf("%d",&M);
rap(i,1,M)
{
scanf("%d",&arr[i]);
sor[i]=arr[i];
}
sort(sor+1,sor+1+M);
int now=1;
Hash[sor[1]]=1;
num[1]=sor[1];
rap(i,2,M)
{
if(sor[i]!=sor[i-1])
{
Hash[sor[i]]=++now;
num[now]=sor[i];
}
}
N=now;
rap(i,1,M)
{
UpData(Hash[arr[i]]);
if(i%2==1)printf("%d\n",num[Query(i/2+1)]);
}
}
练习:
三元上升子序列
普通平衡树(你没有看错)
动态开点
动态开点用在一些数据特别大(主要是权值线段树)和线段树合并(我现在还不会)中.动态开点的作用
在一些用到权值线段树的题目中数据如果数据大于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);//将合并后的线段树上的值合并
}
线段树合并的复杂度
可以发现线段树合并时如果两颗树都有的部分需要全部扫一遍,有一棵树没有的部分可以直接返回,所以它的复杂度为两树在同一位置都有节点的节点的个数.标记永久化
普通的线段树需要PushUp和PushDown,那有没有存在一种线段树在修改时不用到他们呢,标记永久化就出现了.对于每次修改就在修改的最上层的点上打上标记,在查询时就只需要一路向下将标记合并就行了,主要用在主席树这种不适合修改的数据结构上.
没有写过什么题,具体可以看其他大佬的博客.二维线段树
二维线段树有两种写法,四分树和树套树,根据dalao的说法,四分树非常的菜很容易被卡,最坏时修改需要O(N),所以以下内容主要讲树套树的写法.二维线段树的作用
顾名思义,用来修改二维平面上的东西,树套树是在每个节点上再开一棵线段树,这样每次修改和查询的时间复杂度就是\(O(N\log^2_2N)\)(大概).二维线段树的写法
拿出一道模板题.
区间最大值+区间覆盖(但是这里的区间是一个矩形),对于线段树其实没什么变化,就是要用上标记永久化,据说二维线段树不支持打标记.代码(我自然不会全部放出来)
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中的\(\textcolor{blue}{题解}\),里面的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