线段树小记

日常废话:

不知道为什么对water_lift的线段树板子有着迷之执着,就是想用water_lift的板子。(但是忘记water_lift怎么写的了然后又找不到类似的板子只能与一本通为伍了qwq)

 


线段树?是什么:

线段树是一种数据结构,用来维护一段区间的某个特征(比如最小值、和、积……)的二叉树。最简单的应用大概就是求和、维护最小值一类,当然线段树复杂起来让你想象不到。

线段树的几个基本操作:

以求区间和的区间查询,单点修改,区间修改为eg

首先的说,其实本来想先写一个朴素的会炸的区间修改的,结果我不会emm,所以直接加lazy_tag了。

Lazy_tag:

延迟标记,如果区间修改时我们递归到叶节点在进行修改的话,复杂度是O(mlogn),比朴素还要慢,因此有延迟下传的标记,当我们暂时用不到此子树时,不需进行更新,只需要在树根上打一个标记,然后维护此根节点的值。当用到此子树时,再进行下传。

假装所有的基本操作都已经写好了:

#include<bits/stdc++.h>
#define ll long long

using namespace std;
inline ll read(){
    ll ans=0;
    char last=' ',ch=getchar();
    while(ch>'9'||ch<'0') last=ch,ch=getchar();
    while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
    if(last=='-') ans=-ans;
    return ans;
}
const ll N=100005;

ll n,m;
ll val[N];
ll sum[N*4],laz[N*4];

int main(){
    n=read();m=read();
    for(int i=1;i<=n;i++)
      val[i]=read();
    build(1,1,n);
    ll ops,x,y,k;
    for(int i=1;i<=m;i++){
        ops=read();x=read();y=read();
        if(ops==1){
            k=read();
            Add(1,1,n,x,y,k);
        }
        else printf("%lld\n",query(1,1,n,x,y));
    }
    return 0;
}

1.建树;

“你在写什么啊?”

“线段树。”

“树呢?”

没有树的线段树是没有灵魂的,因此第一步显然是建树;(大概我只会写建树了

建树代码:

void build(ll node,ll x,ll y){
    if(x==y){
        sum[node]=val[x];
        return;
    }
    ll mid=x+y>>1;
    build(node<<1,x,mid);
    build(node<<1|1,mid+1,y);
    sum[node]=sum[node<<1]+sum[node<<1|1];
}

解释一下:

ll node表示当前要建的结点编号,ll x表示此区间的左端点,ll y表示此区间的右端点;

当x==y时,也就是叶子结点,非常容易就可以知道此结点的sum就是它本身的值。

然后因为线段树是非常严格的二叉树,因此它的每个儿子的编号都是确定的,分别为node*2和node*2+1;因此递归的建树,最后维护区间和就好了;

2.区间查询操作:

先上代码:

ll query(ll node,ll l,ll r,ll x,ll y){
    if(l>=x&&r<=y)//递归到的区间完全被包含在要查询区间中
      return sum[node];
    ll mid=l+r>>1;
    ll res=0;
pushdown(node,l,r,mid);
if(x<=mid) res+=query(node<<1,l,mid,x,y); if(mid<y) res+=query(node<<1|1,mid+1,r,x,y); return res; }

解释一波:

因为查询时一定是从1~n开始查询,然后递归查询。所以当递归到一个完全被包含在要查询区间的数,显然可以不用向下继续递归了,因为这个点已经维护了这段区间的和,直接返回就可以;然后判断区间中点,显然如果出现:

x<=mid的情况,也就表示我们可以在node 的左儿子中查询到一个区间(区间的大小我们无法确定,可能是整个左儿子,也可能是左儿子的一个叶节点),那么就需要去递归的求左儿子

 y>mid的情况,也就说明在右儿子中一定包含一部分是在区间[x,y]中,所以需要递归右儿子;

 然后将左儿子和右儿子递归到的值加起来,就是我们要查询的区间的和;

3.单点修改(不会写需现学)

CODE:

void change(ll node,ll l,ll r,ll x,ll v){//单点加v 
    if(r<x||l>x) return;//当右区间比要修改的x的值要小时,显然递归右区间没有用
    //左区间同理
    if(l==r&&l==x){
        sum[node]+=v;
        return; 
    } 
    ll mid=l+r>>1;
    change(node<<1|1,mid+1,r,x,v);
    change(node<<1,l,mid,x,v);
    sum[node]=sum[node<<1]+sum[node<<1|1]; 
}

解释一波~

首先判断不可能有解的情况:当前递归到的区间的最右要比x小时

显然再递归下去也不可能找到解,所以直接return好啦;

同样当递归到的区间的最左比x大时,显然也不行:

因此这两种情况都直接return;

当递归到了我们需要修改的x时:修改对应sum值,返回。

然后对于不是需要修改的x但是将x包含在内的区间,我们分别递归的求它的左右儿子,最后还有重新维护sum的值;

4.区间修改:

先上代码++:

void add(ll node,ll l,ll r,ll k){
    laz[node]+=k;
    sum[node]+=(r-l+1)*k; 
}
void pushdown(ll node,ll l,ll r,ll mid){
    if(laz[node]==0) return;
    add(node<<1,l,mid,laz[node]);
    add(node<<1|1,mid+1,r,laz[node]);
    laz[node]=0;
}

void Add(ll node,ll l,ll r,ll x,ll y,ll k){
    if(x<=l&&y>=r) return add(node,l,r,k);
    ll mid=l+r>>1;
    pushdown(node,l,r,mid);
    if(x<=mid) Add(node<<1,l,mid,x,y,k);
    if(y>mid) Add(node<<1|1,mid+1,r,x,y,k);
    sum[node]=sum[node<<1]+sum[node<<1|1];
} 

开始解释:

首先判断,如果当前的递归区间完全包含在区间[x,y]中,直接对这个区间进行add操作,维护这个区间的区间和,然后打lazy_tag;

 

然后还是相应的分别递归左儿子和右儿子,还是相应的判断是否需要递归。最后不要忘记维护区间的和。

然后确实没有搞明白为啥此时要pushdown。

 pushdown就是标记下传的过程;


CODE:

#include<bits/stdc++.h>
#define ll long long
const int N=100005;

using namespace std;

inline ll read(){
    int ans=0;
    char last=' ',ch=getchar();
    while(ch>'9'||ch<'0') last=ch,ch=getchar();
    while(ch<='9'&&ch>='0') ans=(ans<<3)+(ans<<1)+ch-'0',ch=getchar();
    if(last=='-') ans=-ans;
    return ans;
}

ll n,m;
ll val[N],sum[N*4],laz[N*4];

void build(ll node,ll x,ll y){//建树
    if(x==y){
        sum[node]=val[x];
        return;
    }
    ll mid=x+y>>1;
    build(node<<1,x,mid);
    build(node<<1|1,mid+1,y);
    sum[node]=sum[node<<1]+sum[node<<1|1];
}

void add(ll node,ll l,ll r,ll k){//添加维护 
    laz[node]+=k;
    sum[node]+=(r-l+1)*k; 
}
void pushdown(ll node,ll l,ll r,ll mid){//标记下传 
    if(laz[node]==0) return;
    add(node<<1,l,mid,laz[node]);
    add(node<<1|1,mid+1,r,laz[node]);
    laz[node]=0;
}

ll query(ll node,ll l,ll r,ll x,ll y){//查询 
    if(l>=x&&r<=y)//递归到的区间完全被包含在要查询区间中
      return sum[node];
    ll mid=l+r>>1;
    ll res=0;
    pushdown(node,l,r,mid); 
    if(x<=mid) res+=query(node<<1,l,mid,x,y);
    if(mid<y) res+=query(node<<1|1,mid+1,r,x,y);
    return res; 
}

void change(ll node,ll l,ll r,ll x,ll v){//单点加v 
    if(r<x||l>x) return;//当右区间比要修改的x的值要小时,显然递归右区间没有用
    //左区间同理
    if(l==r&&l==x){
        sum[node]+=v;
        return; 
    } 
    ll mid=l+r>>1;
    change(node<<1|1,mid+1,r,x,v);
    change(node<<1,l,mid,x,v);
    sum[node]=sum[node<<1]+sum[node<<1|1]; 
}

void Add(ll node,ll l,ll r,ll x,ll y,ll k){//区间加k 
    if(x<=l&&y>=r) return add(node,l,r,k);
    ll mid=l+r>>1;
    pushdown(node,l,r,mid);
    if(x<=mid) Add(node<<1,l,mid,x,y,k);
    if(y>mid) Add(node<<1|1,mid+1,r,x,y,k);
    sum[node]=sum[node<<1]+sum[node<<1|1];
} 

int main(){
    n=read();m=read();
    for(ll i=1;i<=n;i++) val[i]=read();
    build(1,1,n);
    int ops;
    ll x,y,k;
    for(ll i=1;i<=m;i++){
        ops=read(); x=read();y=read();
        if(ops==1){
            k=read();
            Add(1,1,n,x,y,k);
        }
        else printf("%lld",query(1,1,n,x,y));
    }
    return 0;
}

对了注意开空间要开序列长度N的4倍,为啥我也不是很清楚,反正不能开二倍。

好了没有了

end-

 

posted @ 2019-06-29 16:54  Sweetness  阅读(141)  评论(0编辑  收藏  举报