线段树详解

Posted stungyep

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线段树详解相关的知识,希望对你有一定的参考价值。

线段树及其应用

线段树的几个基础操作:建树,单点查询,单点修改,区间查询,区间修改。其代码的主要思想为二分。参考博客:https://blog.csdn.net/qq_39826163/article/details/81436440

数据结构:

struct node
{
    int l;                  //左端点
    int r;                  //右端点
    int sum;                //区间和,因题目而异
    int f;            //懒标记
}tree[4*maxn+1];

1.建树

建树的过程分为三步:1:给定左右端点的确定范围;2:如果是叶子结点,储存需要维护的信息;3:状态合并。下面是实现代码:

void build(int l,int r,int cur)
{
    tree[cur].l = l,tree[cur].r = r;
    if(tree[cur].l == tree[cur].r){
        tree[cur].sum = arr[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(l, mid, cur << 1);
    build(mid + 1, r, cur << 1 | 1);
    pushup(cur);                    //状态合并
}

2.单点查询

单点查询与二分查询法基本一致,如果当前枚举的点左右端点相等,即叶子节点,就是目标节点。如果不是,因为这是二分法,所以设查询位置为x,当前结点区间范围为了l,r,中点为mid,则如果x<=mid,则递归它的左孩子,否则递归它的右孩子。下面是实现代码:

int ask(int pos,int cur)            //cur为当前结点,x为待查位置
{
    if(tree[cur].l==tree[cur].r)        return tree[k].value;
    int mid=(tree[cur].l+tree[cur].r)>>1;
    if(pos<=mid) ask(pos,cur<<1);       
    else ask(pos,cur<<1|1);             //递归左右孩子
}

3.单点修改

和单点查询原理类似,结合建树过程,我们以增加某一个区间的长度代码为例,下面是实现代码:

void modify(int pos,int x,int cur)              //在pos位置修改x(增加x),cur为当前结点编号
{
    if(tree[cur].l == tree[cur].r){
        tree[cur].sum += x;
        return;
    }
    if(tree[cur].f) pushdown(cur);
    int mid = (tree[cur].l + tree[cur].r) >> 1;
    if(pos <= mid)  modify(pos,x,cur<<1);
    else            modify(pos,x,cur<<1|1);
    pushup(cur);
}

4.区间查询

区间查询分为三种状态,1、当前结点区间的值全部为答案的一部分,;2、当前结点区间只有一部分是答案,3、当前结点区间包含了待查询的区间,根据x,y与mid的情况往下走。

即mid=(l+r)/2

y<=mid ,即 查询区间全在,当前区间的左子区间,往左孩子走;x>mid 即 查询区间全在,当前区间的右子区间,往右孩子走否则,两个子区间都走。

下面是实现代码:

void query(int l,int r,int cur)           //l,r为待查询区间
{
    if(l <= tree[cur].l && tree[cur].r <=r ){
        ans += tree[cur].sum;
        return;
    }
    if(tree[cur].f) pushdown(cur);      //将更新信息传递给左右子树
    int mid = (tree[cur].l + tree[cur].r) >> 1;
    if(l <= mid) query(l,r,cur<<1);
    if(mid < r)  query(l,r,cur<<1|1);
}

5.区间修改

如果要修改一个区间的值,给一个区间内的每个数都加或减或修改时,如果我们只想查询某一个子区间的值,如修改[1,100]而只查询[1,2]的值,如果给所有区间都改得画,在树的深度很高的情况下会很浪费。所以这里引入了一个新状态——懒标记,其作用是存储到这个节点的修改信息,暂时不把修改信息传到子节点。就像家长扣零花钱,你用的时候才给你,不用不给你。下面是懒标记的具体实现过程:

懒标记下移:

void pushdown(int cur)
{
    tree[cur<<1].f+=tree[cur].f;
    tree[cur<<1|1].f+=tree[cur].f;
    tree[cur<<1].sum+=tree[cur].f*(tree[cur<<1].r-tree[cur<<1].l+1);
    tree[cur<<1|1].sum+=tree[cur].f*(tree[cur<<1|1].r-tree[cur<<1|1].l+1);
    tree[cur].f=0;
}

还有上面提到的pushup函数,我认为它和oushdown函数一起,是线段树的核心,其他的不过是模板而已,而这个是线段树真正灵活多变的地方,对于任意给定一个题目,你要依据题意,题目需要维护什么,你就维护什么,比如上面一直再说的oushup函数和这里的Pushdown,在这起到的是维护一个区间和的作用。不同题目真正不一样的代码应该就是这两个了。

//以维护区间和为例:
inline void pushup(int cur)
{
    tree[cur].sum=tree[cur<<1].sum+tree[cur<<1|1].sum;
}

区间修改代码:

void modify_interval(int l,int r,int x,int cur)     //[a,b]为待修改的区间,x为区间修改的值
{
    if(tree[cur].l>=l&&tree[cur].r<=r)      //当前区间全部对要修改的区间有用 
    {
        tree[cur].value+=(tree[cur].r-tree[cur].l+1)*x;       //(r-1+1)区间点的总数
        tree[cur].f+=x;
        return;
    }
    if(tree[cur].f)     pushdown(cur);                //懒标记下移
    int mid=(tree[cur].l+tree[cur].r)>>1;
    if(l<=mid)      modify_interval(l,r,x,cur<<1);
    if(r>mid)       modify_interval(l,r,x,cur<<1|1);
    pushup(cur);
}

例题AC代码:poj-2528:题意,每次在[l,r]区间贴广告,最多能看见多少个广告牌?

思路:离散化+线段树区间染色

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cstdlib>

using namespace std;
const int maxn=1e5+50;
struct node
{
    int l,r,num;
}tree[maxn<<2];
int n,T,cnt,tot,li[maxn],ri[maxn];
int point[maxn<<1];
int ans;
bool vis[maxn];
inline void pushdown(int cur)
{
    tree[cur<<1].num=tree[cur].num;
    tree[cur<<1|1].num=tree[cur].num;
    tree[cur].num=0;
}
inline void build(int l,int r,int cur)      //initialization
{
    tree[cur].l=l,tree[cur].r=r;
    if(l==r){
        tree[cur].num=0;
        return;
    }
    int mid=(l+r)>>1;
    build(l,mid,cur<<1);
    build(mid+1,r,cur<<1|1);
    tree[cur].num=0;
}
inline void modify(int l,int r,int x,int cur)
{
    if(tree[cur].l>=l&&tree[cur].r<=r){
        tree[cur].num=x;
        return;
    }
    if(tree[cur].num)   pushdown(cur);
    int mid=(tree[cur].l+tree[cur].r)>>1;
    if(l<=mid)  modify(l,r,x,cur<<1);
    if(mid<r)   modify(l,r,x,cur<<1|1);
}
inline void query(int l,int r,int cur)
{
    if(tree[cur].num&&!vis[tree[cur].num]){
        vis[tree[cur].num]=1;
        ans++;
        return;
    }
    if(l==r)    return;
    if(tree[cur].num)   pushdown(cur);
    int mid=(l+r)>>1;
    query(l,mid,cur<<1);
    query(mid+1,r,cur<<1|1);
}

int main()
{
    scanf("%d",&T);
    while(T--)
    {
        memset(vis,false,sizeof(vis));
        scanf("%d",&n);
        cnt=ans=0;
        for(int i=1;i<=n;++i)   scanf("%d %d",&li[i],&ri[i]);
        for(int i=1;i<=n;++i){
            point[++cnt]=li[i], point[++cnt]=ri[i];
        }
        sort(point+1,point+cnt+1);
        int now=unique(point+1,point+cnt+1)-(point+1);
        tot=now;
        for(int i=2;i<=now;++i){
            if(point[i]-point[i-1]>1)
                point[++tot]=point[i-1]+1;
        }
        sort(point+1,point+tot+1);
        build(1,tot,1);
        for(int i=1;i<=n;++i){
            int l=lower_bound(point+1,point+tot+1,li[i])-point;
            int r=lower_bound(point+1,point+tot+1,ri[i])-point;     //O(n*logn) algorithm
            modify(l,r,i,1);
        }
        query(1,tot,1);
        printf("%d
",ans);
    }
    system("pause");
}

POJ-3468 题意:裸线段树区间修改+区间查询,注意区间查询的时候别忘了pushdown就好了。

#include<cstdio>
#include<cstdlib>
#include<iostream>

using namespace std;
const int maxn=1e5+50;
typedef long long LL;
int arr[maxn],n,q;
LL ans;
struct node
{
    int l,r,f;
    LL sum;
}tree[maxn<<2];
inline void pushup(int cur)
{
    tree[cur].sum=tree[cur<<1].sum+tree[cur<<1|1].sum;
}
inline void build(int l,int r,int cur)
{
    tree[cur].l=l,tree[cur].r=r;
    tree[cur].sum=tree[cur].f=0;
    if(tree[cur].l==tree[cur].r){
        tree[cur].sum=1LL*arr[l];
        return;
    }
    int mid=(l+r)>>1;
    build(l,mid,cur<<1);
    build(mid+1,r,cur<<1|1);
    pushup(cur);
}
inline void pushdown(int cur)
{
    tree[cur<<1].f+=tree[cur].f;
    tree[cur<<1|1].f+=tree[cur].f;
    tree[cur<<1].sum+=1LL*tree[cur].f*(tree[cur<<1].r-tree[cur<<1].l+1);
    tree[cur<<1|1].sum+=1LL*tree[cur].f*(tree[cur<<1|1].r-tree[cur<<1|1].l+1);
    tree[cur].f=0;
}
inline void query(int l,int r,int cur)
{
    if(l<=tree[cur].l&&tree[cur].r<=r){
        ans+=tree[cur].sum;
        return;
    }
    if(tree[cur].f) pushdown(cur);
    int mid=(tree[cur].l+tree[cur].r)>>1;
    if(mid>=l)  query(l,r,cur<<1);
    if(mid<r)   query(l,r,cur<<1|1);
}
inline void modify(int l,int r,int x,int cur)
{
    if(l<=tree[cur].l&&tree[cur].r<=r){
        tree[cur].f+=x;
        tree[cur].sum+=1LL*x*(tree[cur].r-tree[cur].l+1);
        return;
    }
    if(tree[cur].f) pushdown(cur);
    int mid=(tree[cur].l+tree[cur].r)>>1;
    if(mid>=l)  modify(l,r,x,cur<<1);
    if(mid<r)   modify(l,r,x,cur<<1|1);
    pushup(cur);
}

int main()
{
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;++i)   scanf("%d",&arr[i]);
    build(1,n,1);
    while(q--)
    {
        char t;
        getchar();
        scanf("%c",&t);
        if(t=='Q'){
            ans=0;
            int a,b;
            scanf("%d %d",&a,&b);
            query(a,b,1);
            printf("%lld
",ans);
        }
        else{
            int a,b,c;
            scanf("%d %d %d",&a,&b,&c);
            modify(a,b,c,1);
        }
    }
    system("pause");
}

ps:pushdown可以写在函数最上方,玄学写法我也不懂,先pushdown,wa了可以试试qwq

以上是关于线段树详解的主要内容,如果未能解决你的问题,请参考以下文章

李超树详解

详解权值线段树

数据结构 线段树--权值线段树 详解

线段树详解

线段树 入门详解

线段树数据结构详解