【算法】线段树
Tobiichi_Origami · · 个人记录
线段树是一种支持区间操作的数据结构,用类似于一棵树的方式进行存储,本文将会讲述其基本操作和衍生问题以及算法。
一. 基础线段树
线段树用来维护区间信息,一般有四个基础操作,分别是单点修改,单调查询,区间修改,区间查询。
建树
建树操作和二叉树的建树方法十分相近,每次递归分治建树,将区间一分为二,如果到达叶子区间,则直接将所要维护的信息赋值。
每次赋值后需要进行
void pushup(int u)
{
int tmp=t[u<<1].sum+t[u<<1|1].sum;//u<<1和u<<1|1皆为左右儿子编号
t[u].sum=tmp;
}
void build(int u,int l,int r)//u表示当前区间编号
{
t[u].l=l;t[u].r=r;
if(l==r)
{
t[u].sum=w[l];//w[l]为输入区间下标为l的值
return ;
}
int mid=(l+r)>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
单点修改
首先,我们先要明确修改的位置下标以及修改的数值。
和建树的方法相差不大,每次分治查找位置再左区间还是右区间,依次递归进行处理。若找到了位置,则将要修改的信息直接修改。因为信息已经更新,所以还要进行
void modify(int u,int pos,int val)
{
if(t[u].l==pos&&t[u].r==pos)
{
t[u].sum=val;
return ;
}
int mid=(t[u].l+t[u].r)>>1;
if(pos<=mid) modify(u<<1,pos,val);
else modify(u<<1|1,pos,val);
pushup(u);
return ;
}
单点查询
单点查询和单点修改几乎相同。
首先,需要明确查询的位置。再每次分治查找位置再左区间还是右区间,依次递归进行处理,找到位置后直接返回所要查询的数值。
int query(int u,int pos)
{
if(t[u].l==pos&&t[u].r==pos)
return t[u].sum;
int mid=(t[u].l+t[u].r)>>1,res;
if(pos<=mid) res=query(u<<1,pos,val);
else res=query(u<<1|1,pos,val);
return res;
}
区间查询
首先,区间查询需要明确所要查询的区间范围。
再每次分治查找,如果中点大于等于要查询的区间范围的左端点,就说明左区间在所要查询的区间范围内;果中点小于要查询的区间范围的右端点,就说明右区间在所要查询的区间范围内。
之后如果当前递归到的区间再所要查询的区间范围之内,直接返回当前区间所维护的信息,并将其计入答案。
int query(int u,int l,int r)
{
if(t[u].l>=l&&t[u].r<=r)
return t[u].sum;
int mid=(t[u].l+t[u].r)>>1,res=0;
if(l<=mid) res+=query(u<<1,l,r);
if(mid<r) res+=query(u<<1|1,l,r);
return res;
}
区间修改
我们先考虑,如果每个点都要进行一次单点修改,时间复杂度为
我们会引进一个新的概念,懒标记。
我们考虑,更新一定是为了保证答案的准确,所以如果不查询的这个区间,这个区间就可以不更新,换句话说,可以先将之前所要维护的值计入懒标记当中,当这个区间被查询时,将这个区间所维护的信息更新。
区间修改需要明确所修改的区间范围和要修改的值。
和区间查询一样,分治查找并且更新,因为之后还要更新儿子,所以我们要将懒标记传给儿子区间,自己清零,即为
因为区间修改修改了维护的信息,所以需要进行
void pushdown(int u)
{
if(t[u].add)
{
t[u<<1].add+=t[u].add;
t[u<<1].sum+=(t[u<<1].r-t[u<<1].l+1)*t[u].add;
t[u<<1|1].add+=t[u].add;
t[u<<1|1].sum+=(t[u<<1|1].r-t[u<<1|1].l+1)*t[u].add;
t[u].add=0;
}
return ;
}
void modify(int u,int l,int r,int val)
{
if(t[u].l>=l&&t[u].r<=r)
{
t[u].sum+=(t[u].r-t[u].l+1)*val;
t[u].add+=val;
return ;
}
int mid=(t[u].l+t[u].r)>>1;
if(l<=mid) modify(u<<1,l,r,val);
if(mid<r) modfiy(u<<1|1,l,r,val);
return ;
}
题型
1.线段树优化
在进行某些区间操作时,无法用线段树来进行维护,这时我们就可以每个点进行单调修改,并根据所进行的区间操作特性进行优化。
以花神游历各国为例
题意
给你两个操作,一个是将整个区间开方,另一个是查询一个区间的区间和,用线段树维护。
思路
首先,区间开方用线段树非常难维护(维护不了?),所以我们考虑一些优化。
因为
考虑维护一个区间最大值,因为如果区间最大值已经为
那么如果已经为
区间查询照常进行。
#include<bits/stdc++.h>
#define int long long
using namespace std;
struct tree{
int l,r;
int sum=0;
int max=-114514;
}t[1000001];
int a[100001];
int n,m;
void pushup(int u)
{
t[u].sum=t[u<<1].sum+t[u<<1|1].sum;
t[u].max=max(t[u<<1].max,t[u<<1|1].max);
}
void build(int u,int l,int r)
{
t[u].l=l;t[u].r=r;
if(l==r)
{
t[u].sum=t[u].max=a[l];
return ;
}
int mid=(l+r)>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
void modify(int u,int l,int r)
{
if(t[u].l==t[u].r)
{
t[u].sum=sqrt(t[u].sum);
t[u].max=sqrt(t[u].max);
return ;
}
int mid=(t[u].l+t[u].r)>>1;
if(mid>=l&&t[u<<1].max!=1)
modify(u<<1,l,r);
if(mid<r&&t[u<<1|1].max!=1)
modify(u<<1|1,l,r);
pushup(u);
return ;
}
int query(int u,int l,int r)
{
if(t[u].l>=l&&t[u].r<=r)
return t[u].sum;
int mid=(t[u].l+t[u].r)>>1,res=0;
if(mid>=l) res+=query(u<<1,l,r);
if(mid<r) res+=query(u<<1|1,l,r);
return res;
}
signed main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++) cin>>a[i];
build(1,1,n);
scanf("%lld",&m);
while(m--)
{
int op;
scanf("%lld",&op);
if(!op)
{
int x,y;
scanf("%lld %lld",&x,&y);
if(x>y) swap(x,y);
modify(1,x,y);
}
else
{
int x,y;
scanf("%lld %lld",&x,&y);
if(x>y) swap(x,y);
printf("%lld\n",query(1,x,y));
}
}
}
总结
此类题型需多加注意区间修改的特性,根据这个特性进行优化。
习题
The Child and Sequence
2.标记永久化
标记永久化是一个线段树优化的技巧,此技巧可以省略线段树中的
以区间和为例,考虑每次修改区间时,将其子树所被影响到的区间打上标记,在更新时,将所有子树中的标记累加,这样也就省去了朴素线段树中下传标记的操作。
二. 权值线段树
权值线段树是线段树的一种衍生算法,其基本存储结构和线段树基本相同。
如图,每个结点存储的分别为下方元素的出现次数。
1.建树
和朴素线段树的建树方式相同,只是其维护的信息与朴素线段树不同。
void pushup(int u)
{
int tmp=t[u<<1].sum+t[u<<1|1].sum;
t[u].sum=tmp;
}
void build(int u,int l,int r)
{
t[u].l=l;t[u].r=r;
if(l==r)
{
t[u].sum=w[l];//w[l]为l出现的次数
return ;
}
int mid=(l+r)>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
2.单点修改
单点修改就和朴素线段树一模一样了,因为都是修改一个数,本质上没有区别。
void modify(int u,int pos,int val)
{
if(t[u].l==pos&&t[u].r==pos)
{
t[u].pos+=val;
return ;
}
int mid=(t[u].l+t[u].r)>>1;
if(mid>=l) modify(u<<1,pos,val);
if(mid<r) modify(u<<1|1,pos,val);
pushup(u);
}
3.单点查询
单点查询查询的是
int query(int u,int pos)
{
if(t[u].l==pos&&t[u].r==pos)
return t[u].sum;
int mid=(t[u].l+t[u].r)>>1,res;
if(pos<=mid) res=query(u<<1,pos,val);
else res=query(u<<1|1,pos,val);
return res;
}
4.查询
首先,我们要明确,此操作只针对于整个权值线段树,不适用于某个独立的子区间。
我们考虑,我们存入的左右端点皆为某个具体的数字,且升序排序,那么我们便可以分治求解答案。
如果
int query(int u,int k)
{
if(t[u].l==k&&t[u].r==k)
return t[u].l;
int mid=(t[u].l+t[u].r)>>1;
if(k<mid) return query(u<<1,k);
else return query(u<<1|1,k-mid);
}
习题
中位数 直接用权值线段树加离散化
送花 权值线段树模板题
三. 动态开点线段树
因为朴素的线段树需要开四倍空间,很容易空间超限,那么我们此时便可以运用动态开点线段树。
动态开点线段树的主要思想即为如果用到了便开一个点,所以动态开点线段树也没有建树的操作。每次需要一个点便将节点编号增加,然后插入数值。
单点修改
int pushup(int u)
{
int tmp=t[t[u].l].sum+t[t[u].r].sum;
t[u].sum=tmp;
}
void modify(int &u,int l,int r,int pos,int val)
{
if(!u) u=++cnt;
if(l==pos&&r==pos)
{
t[u].sum=x;
return ;
}
int mid=(l+r)>>1;
if(pos<=mid) modify(t[u].l,l,mid,pos,val);
else modify(t[u].r,mid+1,r,pos,val);
pushup(u);
}
单点查询
int query(int u,int l,int r,int pos)
{
if(!u) return 0;
if(l==pos&&r==pos)
return t[u].sum;
int mid=(l+r)>>1,res=0;
if(pos<=mid) res=query(t[u].l,l,mid,pos);
else res=query(t[u].r,mid+1,r,pos);
return res;
}
区间查询
int query(int u,int l,int r int L,int R)
{
if(!u) return 0;
if(l>=L&&r<=R)
return t[u].sum;
int mid=(l+r)>>1,res=0;
if(mid>=L) res+=query(t[u].l,l,mid,L,R);
if(mid<R) res+=query(t[u].r,mid+1,r,L,R);
return res;
}
区间修改
void pushdown(int u,int l,int r)
{
if(t[u].add)
{
if(!t[u].l) t[u].l=++cnt;
if(!t[u].r) t[u].r=++cnt;
int mid=(l+r)>>1;
t[t[u].l].add+=t[u].add;
t[t[u].l].sum+=(mid-l+1)*t[u].add;
t[t[u].r].add+=t[u].add;
t[t[u].r].sum+=(r-mid)*t[u].add;
t[u].add=0;
}
}
void modify(int &u,int l,int r,int L,int R,int val)
{
if(!u) u=++cnt;
if(l>=L&&r<=R)
{
t[u].add+=val;
t[u].sum+=(r-l+1)*val;
return ;
}
pushdown(u,l,r);
int mid=(l+r)>>1;
if(mid>=L) modify(t[u].l,l,mid,L,R,val);
if(mid<R) modify(t[u],r,mid+1,r,L,R,val);
pushup(u);
}
总结
动态开点线段树准确来说是一种优化线段树空间的技巧,其作用也可以通过离散化来实现。
习题
[SDOI2014]旅行 建