一:线段树的介绍:
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。 [1]
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩。
线段树是建立在线段的基础上,每个结点都代表了一条线段[a,b]。长度为1的线段称为元线段。非元线段都有两个子结点,左结点代表的线段为[a,(a + b) / 2],右结点代表的线段为[((a + b) / 2)+1,b]。
长度范围为[1,L] 的一棵线段树的深度为log (L) + 1。这个显然,而且存储一棵线段树的空间复杂度为O(L)。
线段树支持最基本的操作为插入和删除一条线段。下面以插入为例,详细叙述,删除类似。
将一条线段[a,b] 插入到代表线段[l,r]的结点p中,如果p不是元线段,那么令mid=(l+r)/2。如果b<mid,那么将线段[a,b] 也插入到p的左儿子结点中,如果a>mid,那么将线段[a,b] 也插入到p的右儿子结点中。
插入(删除)操作的时间复杂度为O(logn)。
具体如下图所示:
二:线段树的建立:
递归建立:
先定义一个结构体来存储每一个节点的信息:
代码如下:比如数组num[N]中共有N个元素
struct NODE{
int L,R,sum,lazy;
NODE(){
L=0,R=0,sum=0,lazy=0;
}
}node[4*N];
然后进行递归:
void build(int now,int l,int r){
node[now].L=l,node[now].R=r;//记录节点所求得区间
if(l==r){//l=r说明为叶子节点,直接按原数组赋值即可
node[now].sum=num[r];return;
}
int mid=l+r>>1;
build(now<<1,l,mid);//递归左子树
build(now<<1|1,mid+1,r);//递归右子树
node[now].sum=node[now<<1].sum+node[now<<1|1].sum;//更新节点的信息
}
通过以上两个操作就把一个线段树建立好了。
三:线段树的修改:线段树的修改只需要对修改数值所涉及到的地方进行修改即可,所以只需要递归判断所属区间进行更新。
1:单点修改:num[p]+=q;
void update(int now,int p,int q){
if(node[now].L==node[now].R){//如果是叶子节点,直接加即可
node[now].sum+=q;return ;
}
int mid=node[now].L==node[now].R>>1;
if(mid>=p)update(now<<1,node[now].L,mid,p,q);//找到需要更新的区间
else update(now<<1|1,mid+1,node[now].R,p,q);
node[now].sum=node[now<<1].sum+node[now<<1|1].sum;//更新节点和
}
2:区间修改:
想要进行区间修改需要借助懒标记:
懒惰标记的含义:
本区间已经被更新过了,但是子区间却没有被更新过,被更新的信息是什么区间求和只用记录有没有被访问过,而区间加减乘除等多种操作的问题则要记录进行的是哪一种操作。
这里再引入两个很重要的东西:相对标记和绝对标记。
相对标记和绝对标记
相对标记指的是可以共存的标记,且打标记的顺序与答案无关,即标记可以叠加。 比如说给一段区间中的所有数字都+a,我们就可以把标记叠加一下,比如上一次打了一个+1的标记,这一次要给这一段区间+2,那么就把+1的标记变成+3。
绝对标记是指不可以共存的标记,每一次都要先把标记下传,再给当前节点打上新的标记。这些标记不能改变次序,否则会出错。 比如说给一段区间的数字重新赋值,或是给一段区间进行多种操作。
有了懒惰标记这种神奇的东西,我们区间修改时就可以偷一下懒,先修改当前节点,然后直接把信息挂在节点上就可以了!
如下面这棵线段树,当我们要修改区间[1…4],将元素赋值为1时,我们可以先找到所有的整个区间都要被修改的节点,显然是储存区间[1…3]和[4…4]的这两个节点。我们就可以先把[1…3]的sum改为3:((3−1+1)∗1=3,把[4…4]的sum改为1:(1−1+1)∗1=1,然后给它们打上值为1的懒惰标记,然后就可以了。
具体还要多理解。
懒标记与区间更新的实现:
代码如下:
//下传懒标记
void pushdown(int now){
if(node[now].L==node[now].R){//如果该节点的左右相等,说明为叶子节点,直接将懒标记变为0即可
node[now].lazy=0;return;
}
node[now<<1].lazy+=node[now].lazy;//否则更新左右儿子的懒标记
node[now<<1|1].lazy+=node[now].lazy;
node[now<<1].sum+=(node[now<<1].R-node[now].L+1)*node[now].lazy;//对左右儿子的值进行更新
node[now<<1|1].sum+=(node[now<<1|1].R-node[now<<1|1].L+1)*node[now].lazy;
node[now].lazy=0;//将该位置的懒标记的值清0
}
//对区间l~r之间每一个值加上v
void update(int now,int l,int r,int v){
if(node[now].L>=l&&node[now].R<=r){//如果所加区间覆盖了整个区间,直接加即可
node[now].sum+=(node[now].R-node[now].L+1)*v;
node[now].lazy+=v;
return;
}
if(node[now].lazy)pushdown(now);//下传标记
int mid=node[now].L+node[now].R>>1;
if(l>mid)update(now<<1|1,l,r,v);//如果l大于mid,说明需要更新右子树
else if(r<=mid)update(now<<1,l,r,v);//如果r<=mid说明需要更新左子树
else update(now<<1|1,mid+1,r,v),update(now<<1,l,mid,v);//否则说明需要更新的区间横跨mid,需要对mid的左右分别更新
node[now].sum=node[now<<1].sum+node[now<<1|1].sum;//更新节点和
}
四:线段树的询问:
如果是区间修改的话,区间询问需要不断地下传懒标记。
代码如下:
int getsum(int now,int l,int r){
if(node[now].L>=l&&node[now].R<=r)return node[now].sum;//如果询问区间大于节点区间,直接返回
if(node[now].lazy)pushdown(now);//如果当前节点有懒标记,则下传
int mid=node[now].L+node[now].R>>1;
if(mid>=r)return getsum(now<<1,l,r);//如果mid大于等于r,则只需要对左子树求和
else if(mid<l)return getsum(now<<1|1,l,r);//如果mid<r,则只需要对右子树求和
else return getsum(now<<1,l,mid)+getsum(now<<1|1,mid+1,r);//否则对mid的左右子区间求和
}
五:线段树例题:P3372
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll ma=1e5+10;
ll num[ma],N,M,x,y,z,m;
struct NODE{
ll L,R,sum,lazy;
NODE(){
L=0,R=0,sum=0,lazy=0;
}
}node[4*ma];
void change(ll now){//修改节点的值
node[now].sum=node[now<<1].sum+node[now<<1|1].sum;
}
void build(ll now,ll l,ll r){//建立初始线段树
node[now].L=l,node[now].R=r;
if(l==r){
node[now].sum=num[r];return;
}
ll mid=l+r>>1;
build(now<<1,l,mid);
build(now<<1|1,mid+1,r);
change(now);
}
void pushdown(ll now){//下传懒标记
if(node[now].L==node[now].R){
node[now].lazy=0;return;
}
node[now<<1].lazy+=node[now].lazy;
node[now<<1|1].lazy+=node[now].lazy;
node[now<<1].sum+=(node[now<<1].R-node[now].L+1)*node[now].lazy;
node[now<<1|1].sum+=(node[now<<1|1].R-node[now<<1|1].L+1)*node[now].lazy;
node[now].lazy=0;
}
void update(ll now,ll l,ll r,ll v){//更新区间值
if(node[now].L>=l&&node[now].R<=r){
node[now].sum+=(node[now].R-node[now].L+1)*v;
node[now].lazy+=v;
return;
}
if(node[now].lazy)pushdown(now);
ll mid=node[now].L+node[now].R>>1;
if(l>mid)update(now<<1|1,l,r,v);
else if(r<=mid)update(now<<1,l,r,v);
else update(now<<1|1,mid+1,r,v),update(now<<1,l,mid,v);
change(now);
}
ll getsum(ll now,ll l,ll r){//区间求和
if(node[now].L>=l&&node[now].R<=r)return node[now].sum;
if(node[now].lazy)pushdown(now);
ll mid=node[now].L+node[now].R>>1;
if(mid>=r)return getsum(now<<1,l,r);
else if(mid<l)return getsum(now<<1|1,l,r);
else return getsum(now<<1,l,mid)+getsum(now<<1|1,mid+1,r);
}
int main(){
cin.sync_with_stdio(false);//输入优化
cin>>N>>M;
for(ll i=1;i<=N;i++)cin>>num[i];
build(1,1,N);
while(M--){
cin>>x;
if(x==1){
cin>>y>>z>>m;
update(1,y,z,m);
}else{
cin>>y>>z;
cout<<getsum(1,y,z)<<endl;
}
}
return 0;
}
模板题二:
P3373 【模板】线段树 2
加了一个区间乘,维护的规则是先乘后加。
代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll N=1e5+10;
ll n,m,k,x,y,t,p;
ll a[N];
ll read(){
ll y=0,flag=1;
char ch=getchar();
while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
if(ch=='-')flag=-1,ch=getchar();
while(ch>='0'&&ch<='9')y=y*10+ch-'0',ch=getchar();
return y*flag;
}
struct Node{
ll l,r,sum,add,mul;
}node[4*N];
void change(ll now){
node[now].sum=(node[now<<1].sum+node[now<<1|1].sum)%p;
}
void build(ll now,ll lef,ll rig){
node[now].l=lef,node[now].r=rig,node[now].mul=1;//需要先将乘法标记改成1
if(lef==rig){
node[now].sum=a[lef]%p;
return;
}
ll mid=lef+rig>>1;
build(now<<1,lef,mid);
build(now<<1|1,mid+1,rig);
change(now);
}
void pushdown(ll now){//本题的重点
node[now<<1].sum=(node[now].mul*node[now<<1].sum+(node[now<<1].r-node[now<<1].l+1)*node[now].add)%p;
node[now<<1|1].sum=(node[now].mul*node[now<<1|1].sum+(node[now<<1|1].r-node[now<<1|1].l+1)*node[now].add)%p;
node[now<<1].add=(node[now].add+node[now<<1].add * node[now].mul)%p;//add标记还需要由父亲乘法标记乘上当前的加法标记
node[now<<1|1].add=(node[now].add+node[now<<1|1].add * node[now].mul)%p;
node[now<<1].mul=(node[now<<1].mul%p*node[now].mul)%p;
node[now<<1|1].mul=(node[now<<1|1].mul%p*node[now].mul)%p;
node[now].add=0;
node[now].mul=1;
return;
}
void Add(ll now,ll lef,ll rig,ll v){//区间加
if(node[now].l>rig||node[now].r<lef)return;
if(node[now].l>=lef&&node[now].r<=rig){
node[now].sum=(node[now].sum+(node[now].r-node[now].l+1)*v)%p;
node[now].add=(node[now].add+v)%p;
return;
}
if(node[now].add||node[now].mul!=1)pushdown(now);
ll mid=node[now].r+node[now].l>>1;
if(lef<=mid)Add(now<<1,lef,rig,v);//还有左区间,就更新左区间
if(rig>mid)Add(now<<1|1,lef,rig,v);//还有右区间,就更新右区间
change(now);
}
void Mul(ll now,ll lef,ll rig,ll v){//区间乘
if(node[now].l>rig||node[now].r<lef)return;
if(node[now].l>=lef&&node[now].r<=rig){
node[now].sum=(node[now].sum*v)%p;
node[now].add=(node[now].add*v)%p;
node[now].mul=node[now].mul*v%p;
return;
}
if(node[now].add||node[now].mul!=1)pushdown(now);
ll mid=node[now].r+node[now].l>>1;
if(lef<=mid)Mul(now<<1,lef,rig,v);
if(rig>mid)Mul(now<<1|1,lef,rig,v);
change(now);
}
ll query(ll now,ll lef,ll rig){//区间和
if(node[now].l>rig||node[now].r<lef)return 0;
if(node[now].l>=lef&&node[now].r<=rig){
return node[now].sum%p;
}
if(node[now].add||node[now].mul!=1)pushdown(now);
ll mid=node[now].l+node[now].r>>1;
ll res=0;
if(lef<=mid)res+=query(now<<1,lef,rig);
if(rig>mid)res+=query(now<<1|1,lef,rig);
return res%p;
}
int main(){
cout.tie(0);
n=read(),m=read(),p=read();
for(int i=1;i<=n;i++)a[i]=read();
build(1,1,n);
while(m--){
t=read();
if(t==1){
x=read(),y=read(),k=read();
Mul(1,x,y,k);
}else if(t==2){
x=read(),y=read(),k=read();
Add(1,x,y,k);
}else {
x=read(),y=read();
cout<<query(1,x,y)<<endl;
}
}
return 0;
}
线段树例题:Little Gyro and Array
主要是维护区间等差数列的首相和公差,注意分开成两半之后首相是会变的。
代码如下:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
LL n,m,k,ans,op,x,y,d;
LL f[N];
struct Node{
LL v,a,d,l,r;
}node[4*N];
void change(LL x){
node[x].v=node[x<<1].v+node[x<<1|1].v;
}
void build(LL now,LL L,LL R){
node[now].r=R,node[now].l=L;
if(L==R){
node[now].v=f[R];
return;
}
LL mid=L+R>>1;
build(now<<1,L,mid);
build(now<<1|1,mid+1,R);
change(now);
}
void pushdown(LL now){
LL l1=node[now].l,r1=node[now].r;
LL l2=node[now<<1].l,r2=node[now<<1].r;
LL l3=node[now<<1|1].l,r3=node[now<<1|1].r;
node[now<<1].v=node[now<<1].v+(r2-l2+1)*(node[now].a*2+(r2-l2)*node[now].d)/2;
node[now<<1|1].v=node[now<<1|1].v+(r3-l3+1)*(node[now].a*2+(r2-l2+1)*node[now].d+(r1-l1)*node[now].d)/2;
node[now<<1].a+=node[now].a;
node[now<<1].d+=node[now].d;
node[now<<1|1].a+=(node[now].a+(r2-l2+1)*node[now].d);
node[now<<1|1].d+=node[now].d;
node[now].a=0;
node[now].d=0;
}
void update(LL now,LL L,LL R,LL K,LL D){
if(node[now].l>R||node[now].r<L)return;
if(node[now].l>=L&&node[now].r<=R){
node[now].v=node[now].v+(node[now].r-node[now].l+1)*((node[now].l-L)*D+K+K+(node[now].r-L)*D)/2;
node[now].a+=(K+(node[now].l-L)*D);
node[now].d+=D;
return;
}
if(node[now].a||node[now].d)pushdown(now);
LL mid=node[now].l+node[now].r>>1;
if(L<=mid)update(now<<1,L,R,K,D);
if(R>mid)update(now<<1|1,L,R,K,D);
change(now);
}
LL query(LL now,LL L){
if(node[now].l>=L&&node[now].r<=L){
return node[now].v;
}
if(node[now].a||node[now].d)pushdown(now);
LL mid=node[now].l+node[now].r>>1;
if(L<=mid)return query(now<<1,L);
else return query(now<<1|1,L);
}
void solve(){
scanf("%lld%lld",&n,&m);
for(LL i=1;i<=n;i++)scanf("%lld",&f[i]);
build(1,1,n);
while(m--){
scanf("%lld",&op);
if(op==1){
scanf("%lld%lld%lld%lld",&x,&y,&k,&d);
update(1,x,y,k,d);
}else {
scanf("%lld",&x);
printf("%lld\n",query(1,x));
}
}
}
int main(){
solve();
return 0;
}
有了线段树,可以非常方便的维护区间的最大值,最小值等。
但是在维护区间最大和最小值上,ST表(RMQ)可以做到比线段树更优的时间和空间复杂度,可以做到O(nlogn)预处理(可以做到O(n)预处理,但是我不会),O(1)查找,而且常数非常小。
一:ST表(RMQ)介绍:
实践中最常用的就是Tarjan的Sparse-Table算法。
令d[i][j]表示从i开始长度为2^j次方的一段区间元素的最小值,则可以用递推公式计算:d[i][j]=max or min(d[i][j-1],d[i+(1<<(j-1))][j-1])。
假设有n个元素,那么2^j次方要小于等于n,因此d数组的元素个数不超过nlogn个,而且每一项都可以在常数时间内计算完毕,所以总时间不超过nlogn。
预处理代码如下:
for(int i=1;i<=n;i++)cin>>d[i][0];//先处理j=0的时候,肯定就是自身了
for(int j=1;(1<<j)<=n;j++){
for(int i=1;i+(1<<j)-1<=n;i++){
d[i][j]=max(d[i][j-1],d[i+(1<<(j-1))][j-1]);//求最小值取max就行了
}
}
二:ST表的查询操作:
令k是满足2^k<=R-L+1的最大值,则以L开头,R结尾的两个长度为 2 ^k的区间合起来覆盖了查询区间[L,R]。由于取得是最小值,有些元素被考虑了几次也没有问题。
查询代码如下:假设有m次查询
int query(int L,int R){
int k=log(R-L+1)/log(2);//快速求出k
/*int k=0; //还可以这样求
while(1<<(k+1)<=(R-L+1))k++;*/
return max(d[L][k],d[R-(1<<k)+1][k]);
}
三:模板题:P3865
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int nn=1e5+10;
int k,m,n,x,y;
int d[nn][20];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&d[i][0]);
for(int j=1;(1<<j)<=n;j++){
for(int i=1;i+(1<<j)-1<=n;i++){
d[i][j]=max(d[i][j-1],d[i+(1<<(j-1))][j-1]);
}
}
while(m--){
scanf("%d%d",&x,&y);
k=log(y-x+1)/log(2);
printf("%d\n",max(d[x][k],d[y-(1<<k)+1][k]));
}
return 0;
}
模板题简单应用:P2880
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int nn=5e4+10;
int k,m,n,L,R;
int da[nn][20],xiao[nn][20];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&da[i][0]);
xiao[i][0]=da[i][0];
}
for(int j=1;(1<<j)<=n;j++){//预处理最大和最小区间值
for(int i=1;i+(1<<j)-1<=n;i++){
da[i][j]=max(da[i][j-1],da[i+(1<<(j-1))][j-1]);
xiao[i][j]=min(xiao[i][j-1],xiao[i+(1<<(j-1))][j-1]);
}
}
while(m--){
scanf("%d%d",&L,&R);
k=log(R-L+1)/log(2);
printf("%d\n",max(da[L][k],da[R-(1<<k)+1][k])-min(xiao[L][k],xiao[R-(1<<k)+1][k]));
}
return 0;
}