算法总结之线段树

一:线段树的介绍:
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。 [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;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值