线段树(重制)

该来的还是要来的.理解了树状数组之后想要解决区间更新及询问的问题,就要用到线段树.当然非常骚的树状数组可以过,不过那就是大佬做的事了.线段树其实不是一个很难的东西,因为递归二分的原因,除了push_down操作比较费事,其它都很好背下来.只要理解一点点就好.
update:由于以前写的线段树实在太丑了,不符合我现在的码风,故此把以前的代码全部删掉改成新的.

线段树是什么

先引入一个题:洛谷模板题3372.
就是给一个数组,有两种操作:给一个区间的每个数加k,或让你求一个区间的和.
暴力! n*m,gg.那肯定不行.但是用线段树可以.它的预处理复杂度nlogn,操作复杂度logn,空间复杂度n,就能过了.
我再一次作为灵魂画师为大家讲解一下线段树.假设有一个数组a[11],存储数据的位置为1-10.
这里写图片描述

我又画得很骚,不要在意.
也就是说线段树就是一棵完全二叉树,它的每个叶子结点存储数组元素的值,而它的父节点是两个子节点的和.一共有2*n-1个节点,所以处理的时候数组要用2xN的大小,甚至有些时候要开到4xN.

怎么构建

好的.首先我们先建立线段树.可以知道,每一棵子树根节点是segtree[i],左孩子是segtree[i*2],右孩子是segtree[i*2+1].我们可以用位运算加速计算左孩子和右孩子.
据此可写出函数build.

/*定义yuki这种类型是大小为N<<2|13这么大的数组*/
typedef ll yuki[yuzu<<2|13];
/*以后很多地方都要用到这些定义,我们不妨define一下让代码更简洁.*/
#define le rt<<1
#define ri le|1
#define ls le,l,mid
#define rs ri,mid+1,r
yuki val,lazy;//表示一个结点所储存的值和它的标记(标记到时再说)
/*三个参数分别表示线段树结点下标,目前所build的区间的左端点和右端点.*/
void build(int rt=1,int l=1,int r=n){//大多数情况下所需要build的区间都是1 to n
  if (l==r) val[rt]=a[l];//如果rt代表的区间只有一个结点就是原数组中的第l个.
  else{
    int mid=l+r>>1;
    build(ls),build(rs);//分别递归建造左子树和右子树.
    val[rt]=val[le]+val[ri];//显然这个结点的值就是左边的值加上右边的值.
    }
  }

不会也不要紧,背出之.
然后是询问操作.

ll query(int ql,int qr,int rt=1,int l=1,int r=n){
  if (ql>r||qr<l) return 0;//询问的区间和查找到的区间并没有交集,返回0
  if (ql<=l&&qr>=r) return val[rt];//千万不能反,询问的区间完全包含查找区间,这时直接返回查找区间的和.
  int mid=l+r>>1;
  push_down(rt,l,r);
  return query(ql,qr,ls)+query(ql,qr,rs);
  }
}

诶?这个push_down是什么玩意?

void push_down(int rt,int l,int r){
  if (lazy[rt]){
    int mid=l+r>>1;
    lazy[le]+=lazy[rt],lazy[ri]+=lazy[rt];
    val[le]+=(mid-l+1)*lazy[rt];
    val[ri]+=(r-mid)*lazy[rt];
    lazy[rt]=0;
    }
  }

接下来才是高潮呢!终于要解释lazy是什么了.
为了更新区间,线段树中引入了延迟标记这个概念.这才是线段树的精华所在.
每个节点新增加一个标记,记录这个节点是否进行了某种修改(这种修改操作会影响其子节点),对于任意区间的修改,我们先按照区间查询的方式将其划分成线段树中的节点,然后修改这些节点的信息,并给这些节点标记上代表这种修改操作的标记。在修改和查询的时候,如果我们到了一个节点p,并且决定考虑其子节点,那么我们就要看节点p是否被标记,如果有,就要按照标记修改其子节点的信息,并且给子节点都标上相同的标记,同时消掉节点p的标记。
区间更新举例说明:当我们要对区间[0,2]的叶子节点增加2,利用区间查询的方法从根节点开始找到了非叶子节点[0-2],把它的值设置为1+2 = 3,并且把它的延迟标记设置为2,更新完毕;当我们要查询区间[0,1]内的最小值时,查找到区间[0,2]时,发现它的标记不为0,并且还要向下搜索,因此要把标记向下传递,把节点[0-1]的值设置为2+2 = 4,标记设置为2,节点[2-2]的值设置为1+2 = 3,标记设置为2(其实叶子节点的标志是不起作用的,这里是为了操作的一致性),然后返回查询结果:[0-1]节点的值4;当我们再次更新区间[0,1](增加3)时,查询到节点[0-1],发现它的标记值为2,因此把它的标记值设置为2+3 = 5,节点的值设置为4+3 = 7;
//此处摘至http://blog.sina.com.cn/s/blog_a2dce6b30101l8bi.html
我用自己的话来解释一下吧.

其实就是更新的时候,我们不用直接将修改更新到每一个节点,因为有一些结点可能再也不会被询问到了.
这时我们引入延迟标记,在修改完全覆盖整个区间时只是改一下延迟标记,也就相当于把这棵子树的子节点全都收起来而只留下了根节点.
那么如果下一次修改或询问的时候仅仅是部分覆盖了这棵子树,我们就再把延迟标记下放,把它的子树重新伸出来,就可以继续修改或者询问.注意不同的题的标记也是不同的.`
解释完了以后就可以看区间更新了.

void update(int ql,int qr,int v,int rt=1,int l=1,int r=n){
  if (ql>r||qr<l) return;
  if (ql<=l&&qr>=r){
    val[rt]+=(r-l+1)*v;//本区间的所有数的个数乘v
    lazy[rt]+=v;//标记一下
    }else{
    int mid=l+r>>1;
    push_down(rt,l,r);
    update(ql,qr,v,ls),update(ql,qr,v,rs);
    val[rt]=val[le]+val[ri];
    }
  }

模板

最后是AC洛谷3372模板的代码.

#include<bits/stdc++.h> //Ithea Myse Valgulious
namespace chtholly{
typedef long long ll;
#define re0 register int
#define rec register char
#define rel register ll
#define gc getchar
#define pc putchar
#define p32 pc(' ')
#define pl puts("")
/*By Citrus*/
inline int read(){
  re0 x=0,f=1;rec c=gc();
  for (;!isdigit(c);c=gc()) f^=c=='-';
  for (;isdigit(c);c=gc()) x=x*10+c-'0';
  return x*(f?1:-1);
  }
inline void read(rel &x){
  x=0;re0 f=1;rec c=gc();
  for (;!isdigit(c);c=gc()) f^=c=='-';
  for (;isdigit(c);c=gc()) x=x*10+c-'0';
  x*=f?1:-1;
  }
template <typename mitsuha>
inline int write(mitsuha x){
  if (!x) return pc(48);
  if (x<0) x=-x,pc('-');
  re0 bit[20],i,p=0;
  for (;x;x/=10) bit[++p]=x%10;
  for (i=p;i;--i) pc(bit[i]+48);
  }
}using namespace chtholly;
using namespace std;
const int yuzu=1e5;
typedef ll fuko[yuzu<<2|10];
int n=read(),m=read();

struct segtree{
#define le rt<<1
#define ri le|1
#define ls le,l,mid
#define rs ri,mid+1,r
fuko val,lazy;
void build(int rt=1,int l=1,int r=n){
  if (l==r) val[rt]=read();
  else{
    int mid=l+r>>1;
    build(ls),build(rs);
    val[rt]=val[le]+val[ri];
    }
  }
void push_down(int rt,int l,int r){
  if (lazy[rt]){
    int mid=l+r>>1;
    lazy[le]+=lazy[rt],lazy[ri]+=lazy[rt];
    val[le]+=(mid-l+1)*lazy[rt];
    val[ri]+=(r-mid)*lazy[rt];
    lazy[rt]=0;
    }
  }
void update(int ql,int qr,int v,int rt=1,int l=1,int r=n){
  if (ql>r||qr<l) return;
  if (ql<=l&&qr>=r){
    val[rt]+=(r-l+1)*v;
    lazy[rt]+=v;
    }else{
    int mid=l+r>>1;
    push_down(rt,l,r);
    update(ql,qr,v,ls),update(ql,qr,v,rs);
    val[rt]=val[le]+val[ri];
    }
  }
ll query(int ql,int qr,int rt=1,int l=1,int r=n){
  if (ql>r||qr<l) return 0;
  if (ql<=l&&qr>=r) return val[rt];
  int mid=l+r>>1;
  push_down(rt,l,r);
  return query(ql,qr,ls)+query(ql,qr,rs);
  }
}my_;

int main(){
my_.build();
for (;m--;){
  re0 t=read(),l=read(),r=read();
  if (t==1){
    my_.update(l,r,read());
    }
  else{
    write(my_.query(l,r)),pl; 
    }
  }
}

例题

我看看有没有线段树的例题.有几个.
我再引用一下别人的博客吧.
线段树经典例题

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值