线段树入门

  线段树是一个比较高端的数据结构,与树状数组类似,它也具有维护一个数列的功能,建立于二分思想上,它的基础操作有三种:建树,查询,修改。其中修改和查询包括了单点和区间,单点修改和查询非常简单,所以我们这次重点讲的是区间修改和区间查询,以及建树。

  所谓线段树,是将一段区间作为节点储存成树形的结构,一般都是二叉树,线段树很好理解,但是在刚刚学习中可能会因为代码过于繁琐而屡屡出错,请初学者耐心调试,坚持就是胜利。

  我们需要一个结构体取保存每个节点:

struct line{
    long long lo,hi,w,lazy;
}tree[400040];

 

  lo表示下界,hi表示上界,w表示这一结点上所有元素值得和,lazy时懒标记(后面会讲)

 

1-建树操作:

  我们想要使用线段树,一定要先建树,由于线段树是一个二叉树结构,所以我们对于建树的函数设置三个局部变量:lo,hi,k 分别表示现在这个节点所代表线段的下界lo,上界hi,和节点编号:k,那么这个节点的子节点怎么表示呢,首先,我们需要一个mid来表示(lo+hi)/2的值,这样我们就可以将一个区间一分为二了(lo-mid,mid+1-hi),因为这个区间包含的点不一定是奇数,所以两个子节点的区间长度不一定相等。而k怎么计算呢,我们观察                         1

                      2        3

                    4     5    6     7

  可以大概看出,一个节点k的两个子节点分别是2*k和2*k+1, 我们用线段树的每个节点表示这个区间中的所有元素的和,所以我们递归建树,知道某个节点的hi和lo相等,说明它是单个元素节点,我们就输入它的值然后return,到它的父亲节点时,父亲节点累加它的两个儿子节点的值,然后不断向上递归,直到根节点。

  这就是建树的总过程,下面是代码:

void build(int dwn,int oup,int k){
    tree[k].lo=dwn,tree[k].hi=oup;
    if(dwn==oup){
        cin>>tree[k].w;
        return;
    }
    int mid=(dwn+oup)/2;
    build(dwn,mid,2*k);
    build(mid+1,oup,2*k+1);
    tree[k].w=tree[2*k+1].w+tree[2*k].w;
}

  

 

 

2-区间查询:

  不带懒标记的区间查询非常好理解,我们首先设查询的区间为x,y ,我们从k=1节点开始,向下找子节点,k*2和k*2+1,因为我们已经用结构体记录了哪个节点包括的区间,所以我们判断如果这个节点的整个区间都包含在x,y里,那我们直接累加这个节点的值然后return,如果不在,那么我们计算mid的值,如果x小于mid,那么一定包含了左子节点,我们向下继续找左子节点,在判断如果y大于等于mid+1,那么则包含右子节点,我们就搜右子节点,这样推下去,一定会将每个x,y区间内的节点累计上,此时的结果就是区间所有数和。

下面上代码:

void search(int k){
    int dwn=tree[k].lo;
    int oup=tree[k].hi;
    if(dwn>=x&&oup<=y){
        ans+=tree[k].w;
        return;
    }
    int mid=(dwn+oup)/2;
    if(x<=mid) search(k*2);
    if(y>mid) search(k*2+1);
    
}

 

 

3-区间修改

  我们如果需要区间修改的话,有两种方法,1.我们递归找到每一个单元素节点,然后修改,递归上来时父节点根据子节点的更新来更新自己的值。这种方法虽然可行,但是时间复杂度很高,所以懒标记就诞生了,懒标记懒标记当然懒了,它的宗旨就是,我们修改一个区间的时候,和查询区间一样找到包括在这个区间内的节点,然后打上标记,标记的是这个区间的每个数需要修改的值,我们为了方便,只更新这个区间的值,将这个区间的值加上这个区间内的元素总个数*修改值,这样我们就可以先不让修改向下传,直到使用他们的时候再向下传标记,因为这种方法实在是太懒,所以叫做懒标记。

  虽然它懒,但是它确实节省了不少时间。

  下面我们来介绍这个懒标记如何传递。

  假如k这个点打上了懒标记,然后我们需要访问k的子节点,我们为了不让数据失真,所以必须要让数据下穿,我们将k的子节点2*k,和2*k+1的值加上它们区间内的元素个数*k的懒标记(先假设这个区间修改只有加法),然后给2*k,2*k+1这两个节点打上懒标记然后将k节点的懒标记清零,这就完成了懒标记的传递。

下面上代码:

void down(int k){
    long long po=tree[k].lazy;
    tree[k*2].w+=(tree[k*2].hi-tree[k*2].lo+1)*po;
    tree[k*2+1].w+=(tree[k*2+1].hi-tree[k*2+1].lo+1)*po;
    tree[k*2].lazy+=po;
    tree[k*2+1].lazy+=po;
    tree[k].lazy=0;
}

 

  带懒标记的区间修改也是非常好操作的,我们输入修改的值change,我们就给每个再修改区间内的节点的懒标记都加上change,然后只用将这个区间的值更新就行了,等到用的时候再将懒标记下穿。

下面上代码:

void changed(int k){
    int dwn=tree[k].lo;
    int oup=tree[k].hi;
    if(dwn>=a&&oup<=b){
        tree[k].w+=(tree[k].hi-tree[k].lo+1)*change;
        tree[k].lazy+=change;
        return;
    }
    if(tree[k].lazy) down(k);
    int mid=(dwn+oup)/2;
    if(a<=mid) changed(k*2);
    if(b>mid) changed(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w; //一定记得回来时更新它父亲节点的值
}

 

 

  而打上懒标记的区间的查询和一般的查询也没什么区别,只用再需要用到这一点的子节点时下穿懒标记就好了。

上代码:

void search(int k){
    int dwn=tree[k].lo;
    int oup=tree[k].hi;
    if(dwn>=x&&oup<=y){
        ans+=tree[k].w;
        return;
    }    
    if(tree[k].lazy) down(k);
    int mid=(dwn+oup)/2;
    if(x<=mid) search(k*2);
    if(y>mid) search(k*2+1);
    
}

 

 

所以整个带有建树,区间修改,区间查询的线段树代码就是:

#include<iostream>
#include<cstdio>
using namespace std;
struct line{
    long long lo,hi,w,lazy;
}tree[400040];
int n,m,x,y,a,b,change;
long long ans=0;
void build(int dwn,int oup,int k){
    tree[k].lo=dwn,tree[k].hi=oup;
    if(dwn==oup){
        cin>>tree[k].w;
        return;
    }
    int mid=(dwn+oup)/2;
    build(dwn,mid,2*k);
    build(mid+1,oup,2*k+1);
    tree[k].w=tree[2*k+1].w+tree[2*k].w;
}
void down(int k){
    long long po=tree[k].lazy;
    tree[k*2].w+=(tree[k*2].hi-tree[k*2].lo+1)*po;
    tree[k*2+1].w+=(tree[k*2+1].hi-tree[k*2+1].lo+1)*po;
    tree[k*2].lazy+=po;
    tree[k*2+1].lazy+=po;
    tree[k].lazy=0;
}
void search(int k){
    int dwn=tree[k].lo;
    int oup=tree[k].hi;
    if(dwn>=x&&oup<=y){
        ans+=tree[k].w;
        return;
    }    
    if(tree[k].lazy) down(k);
    int mid=(dwn+oup)/2;
    if(x<=mid) search(k*2);
    if(y>mid) search(k*2+1);
    
}
void changed(int k){
    int dwn=tree[k].lo;
    int oup=tree[k].hi;
    if(dwn>=a&&oup<=b){
        tree[k].w+=(tree[k].hi-tree[k].lo+1)*change;
        tree[k].lazy+=change;
        return;
    }
    if(tree[k].lazy) down(k);
    int mid=(dwn+oup)/2;
    if(a<=mid) changed(k*2);
    if(b>mid) changed(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
int main(){
    ios::sync_with_stdio(false);
    cin>>n>>m;
    build(1,n,1);
    int now;
    for(int i=1;i<=m;i++){
        cin>>now;
        if(now==1){
            cin>>a>>b>>change;
            changed(1);
        }
        else{
            ans=0;
            cin>>x>>y;
            search(1);
            cout<<ans<<endl; 
        } 
    }
}

 

谢谢阅读。

 

转载于:https://www.cnblogs.com/tianbowen/p/11312070.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值