[算法]从0到1快速地学习线段树(递归与非递归实现)

1.什么是线段树?

线段树(Segment Tree)是一种二叉搜索树,与区间树相似,它将一个区间划分为一些单元区间,每个单元区间对应线段树中的一个叶结点。如下图:一个序列:1,5,4,2,3。把它的区间([1,5])建立成含有编号区间的结点的二叉树,这就是线段树。
在这里插入图片描述

2.递归实现线段树的操作

细细地一看,我们会发现一个结点的左子结点的编号为当前结点编号的2倍(2*k或k<<1,k为当前结点编号),而右结点的编号为当前结点编号的2倍+1(2*k+1或k<<1|1)。叶子结点的区间的起点等于区间的终点,叶子结点就是我们序列元素了。到这里我们已经基本了解线段树是什么了,但是我们怎么去建立这个线段树呢?
下面我以洛谷的一道题一步步地去了解怎么运用线段树:线段树模板题
有关结点的数组长度:一般是开到结点值最大*4(maxn<<2)。
sum[maxn<<2]:记录的每个结点区间的每个数的总和。
arr[maxn]:记录的是序列元素(1,5,4,2,3)。
在这里插入图片描述

void build_tree(int k,int L,int R){///建树,初始为:build_tree(1,1,n);
   if(L==R) {///叶子节点,修改
        sum[k]=arr[L];///记录sum值,为求和做准备
        return;
   }
   else{
       int mid = (L+R)>>1;///中间值
       build_tree(k<<1,L,mid);///递归左子树
       build_tree(k<<1|1,mid+1,R);///递归右子树
       sum[k] = sum[k<<1]+sum[k<<1|1];
       ///当前结点的区间每个数和:左子树的每个数和+右子树每个数的和
   }
}

到这里线段树已经建好了,我们接下来就要了解线段树怎么用,有何用?

  • 点更新(针对一个数的修改):假设我们把元素3添加2:3+2=5,我们看看那些区间需要修改的。
///[L,R]为原始区间
///[l,r]为要操作的区间
void update(int L,int R,int x,int l,int r,int k){///初始为update(1,n,v,x,y,1);
        if(l==r){///到达叶结点
           sum[k]+=x;///x为要添加的值
           return;
        }
         int mid = (L+R)>>1;
         ///判断我们是往左子树调用还是往右子树调用
         if(l<=mid)update(L,mid,x,l,r,k<<1);
         if(r>mid)update(mid+1,R,x,l,r,k<<1|1);
         sum[k] = sum[k<<1]+sum[k<<1|1];
         ///当前结点区间内每个数的和=左子树区间内每个数的和+右子树区间内每个数的和
}

到这里我们可能对判断往那个方向调用可能疑惑:指定区间分三种情况:
在这里插入图片描述
L>=mid:适用第二种情况。R>mid:适用第三种情况。第一种情况l→mid部分交给L>=mid去递归,mid→r部分交给R>mid去递归。

  • 区间更新(针对指定区间内每个数的修改):假设我们指定区间[2,3]中每个数+1
///lazy[maxn<<2]:标记结点是否更新。
void PushDown(int k,int l,int r){///下推标记
     if(lazy[k]){///上一个步骤执行过lazy
          lazy[k<<1]+=lazy[k];///其左子结点区间标记
          lazy[k<<1|1]+=lazy[k];///其右子结点区间标记

          sum[k<<1]+=l*lazy[k];///
          sum[k<<1|1]+=r*lazy[k];

          lazy[k]=0;///取消当前结点区间的标记
     }
}

我们可能会疑问这个lazy数组究竟是有什么用? 我看到很多博客把这个结点叫做延迟结点或懒惰结点,不管它叫啥。我只知道:lazy是为了记忆当前结点区间已经更新了,如果原始区间完全在操作区间内我们就更新到了指定区间了,程序可以结束。我们举个例子:假如我们第一个操作就是要更新的区间为[1,3],不断地递归找这个区间,PushDown函数没有执行(由于lazy数组暂时还是0),当我们找到了,把当前区间结点标记,程序结束。

在这里插入图片描述
假设我们第二个操作是更新[1,2]这个区间,当我们找到这个区间为[1,3]的结点时,我们发现l这个结点的lazy不为0,这时我们可以执行PushDown函数,然后当前结点的左结点右结点标记,当前结点取消标记。然后不断递归,只要没有找到,我们继续执行PushDown函数…直到找到这个区间然后才叠加更新,然后标记,程序结束。(到这里应该懂lazy的作用了吧!!!希望对你有帮助)
在这里插入图片描述

void update(int L,int R,int x,int l,int r,int k){///区间更新,初始为update(1,n,v,x,y,1);
        if(l<=L&&R<=r){///如果原始区间完全在操作区间[l,r]内
           sum[k]+=x*(R-L+1);///更新数字和,这就是跟点更新的差别了
           lazy[k]+=x;///为了标记当前结点区间添加一个树
           return;
        }
         int mid = (L+R)>>1;
         PushDown(k,mid-L+1,R-mid);///下推标记
         if(l<=mid)update(L,mid,x,l,r,k<<1);
         if(r>mid)update(mid+1,R,x,l,r,k<<1|1);
         sum[k] = sum[k<<1]+sum[k<<1|1];
}
  • 区间求和(区间查询)
int query(int L,int R,int l,int r,int k){///区间查询求和,初始为query(1,n,x,y,1)
   if(l<=L&&R<=r) return sum[k];
   int mid = (L+R)>>1;
   PushDown(k,mid-L+1,R-mid);///为了保证sum的正确性
   ll ans = 0;
   if(l<=mid) ans+=query(L,mid,l,r,k<<1);
   if(r>mid) ans+=query(mid+1,R,l,r,k<<1|1);
   return ans;
}
  • 区间最值(针对指定区间内所有数的最值):下面以最大值为例。
//segtree[maxn<<2]:记录最值
void build_tree(int k,int L,int R){///建树
   if(L==R) {///叶子节点,修改
        segtree[k]=arr[L];
        return;
   }
   else{
       int mid = (L+R)>>1;///中间值
       build_tree(k<<1,L,mid);
       build_tree(k<<1|1,mid+1,R);
       segtree[k] = max(segtree[k<<1],segtree[k<<1|1]);
       ///左子树的最值与右子树的最值的最值
   }
}
int query(int L,int R,int l,int r,int k){///区间查询求最值
   if(l<=L&&R<=r) return segtree[k];
   int mid = (L+R)>>1;
   ll ans = 0;
   if(l<=mid) ans=max(ans,query(L,mid,l,r,k<<1));
   if(r>mid) ans=max(ans,query(mid+1,R,l,r,k<<1|1));
   return ans;
}

3.非递归实现线段树

能用非递归最好用非递归。再简介之前如果不了解位运算的话可以参考我的另一篇博客:四大运算符:位运算
在这里插入图片描述

  • 建立线段树:
    我们了解了递归地方式后,我们都知道其建树是自上而下,而非递归方式是自下而上
///sum存储区间和
void build_tree(int n){///建树
    N=1;
    while(N<n+2) N<<=1;///扩大两倍;
    for(int i=1;i<=n;i++)
        sum[N+i] = arr[i];
    for(int i=N-1; i>0; i--){
        sum[i] = sum[i<<1]+sum[i<<1|1];
        lazy[i]=0;
    }
}
  • 点更新+区间查询:
    在这里插入图片描述
    我们执行一下程序:求区间[2,4]的和:i=9,j=13。先判断i与j的各父结点不同,只有j可执行添加sum[12]。i=4,j=6,i与j的各父结点不同,只有i可执行,添加sum[5]。i=2,j=3,i与j的父结点相同,程序退出。
void update(int l,int x){
   for(int i=N+l;i;i>>=1)
      sum[i]+=x;///不断地使其父结点+x
}
///i^j^1值为0时i和j的父结点相同,循环跳出
///我们都知道非位运算~(a)=-(a+1)
///~i&1判断左子树,i为偶返回1
///j&1判断右子树,j为奇返回1
int query(int l,int r){
   int ans=0;
   for(int i=N+l-1,j=N+r+1;i^j^1;i>>=1,j>>=1 ){
      if(~i&1) ans+=sum[i^1];///i^1等同于i+1
      if(j&1) ans+=sum[j^1];///j^1等同j-1;
   }
   return ans;
}

从这里我们可以看出,为什么N要大于等于n+2了,因为i要从i+1开始,而j要从j-1开始,所以必须要有一个可以作为基准的起点N和终点N+n+1。所以我们存储都是N+1到N+n。

  • 区间更新+区间查询:
void update(int l,int r,int x){
    int i,j,ln=0,rn=0,count=1;
    ///ln:i一路走来已经包含了几个数
    ///rn:j一路走来已经包含了几个数
    ///count:本层当前结点区间元素个数
    for(i=N+l-1,j=N+r+1; i^j^1; i>>=1,j>>=1,count<<=1)
    {
        sum[i]+=x*ln;
        sum[j]+=x*rn;
        if(~i&1) lazy[i^1]+=x, sum[i^1]+=x*count, ln+=count;
        if(j&1) lazy[j^1]+=x, sum[j^1]+=x*count, rn+=count;
        
    }
    ///更新指定最上层,i与j共同父结点的地方
    for(;i;i>>=1,j>>=1){
        sum[i]+=x*ln;
        sum[j]+=x*rn;
    }
}

int query(int l,int r){
    int i,j,ln=0,rn=0,count=1;
    int ans = 0;
    
    for(i=N+l-1,j=N+r+1; i^j^1; i>>=1,j>>=1,count<<=1)
    {
        if(lazy[i]) ans+=lazy[i]*ln;
        if(lazy[j]) ans+=lazy[j]*rn;
        if(~i&1)ans+=sum[i^1], ln+=count;
        if(j&1) ans+=sum[j^1], rn+=count;
        
    }
     for(;i;i>>=1,j>>=1){
        ans+=lazy[i]*ln;
        ans+=lazy[j]*rn;
    }
    return ans;
}

博主正在不断地学习算法,坚持只要对于每一个算法有新的理解都会不断更新博文的原则,如果有关算法类博文存在不足之处,请大佬们不吝指教!!!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值