线段树学习笔记 2019/8/11

1、线段树简介

 线段树是一种二叉搜索树,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

如果线段树中的一个非叶子节点编号为x,他的左儿子的编号为(x<<1),他的右儿子编号为(x<<1|1)。

2、Some Questions

a)为什么线段树要开4倍空间?  线段树并不一定是完全二叉树,可能会出现以下这种情况:

​​​​图片来自: https://blog.csdn.net/zzti_xiaowei/article/details/84394923

 

有些编号比较小的节点变成了叶节点,但比他大的编号里出现了非叶结点,所以会用到更大的编号。

b)线段树的时间复杂度:用线段树对“编号连续”的一些点,进行修改或者统计操作,修改和统计的复杂度都是O(log n)

c)如何设置lazytag?如何用设置的lazy_tag更新所需数据值?  Lazytag的设置,主要是要求:求sum的时候,根据从父区间传下来的lazy_tag,能正确更新子区间所有内部数据值和他的lazy值,从而正确求出sum

3、线段树的组成

struct node
{
    int l,r,w,f;   ///l,r代表区间,w代表区间和,f为懒标记
}tree[N*4+10];

build 函数

void build(int l,int r,int x)     ///x为当前节点编号
{
    tree[x].l=l;tree[x].r=r;
    if(l==r)
    {
        scanf("%d",&tree[x].w);
        return ;
    }
    build(l,(l+r)/2,x*2);
    build((l+r)/2+1,r,x*2+1);
    tree[x].w=tree[x*2].w+tree[x*2+1].w;
}

区间查询

void sum(int k)
{
    if(tree[k].l>=x&&tree[k].r<=y)///查询[x,y]区间和
    {
        ans+=tree[k].w;
        return;
    }
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) sum(k*2);
    if(y>m) sum(k*2+1);
}

区间修改

修改的时候只修改对查询有用的点,这是区间修改的关键思路。为了实现这个,我们引入一个新的状态——懒标记
递归到这个节点时,只更新这个节点的状态,并把当前的更改值累积到标记中。
当需要递归这个节点的子节点时,标记下传给子节点。

懒标记的下传
①当前节点的懒标记累积到子节点的懒标记中。
②修改子节点状态。原状态+子节点区间点的个数*父节点传下来的懒标记。
③父节点懒标记清0。

a)懒标记下传

void down(int k)
{
    tree[k*2].f+=tree[k].f;
    tree[k*2+1].f+=tree[k].f;
    tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
    tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
    tree[k].f=0;
}

b)区间查询

void add(int k)
{
    if(tree[k].l>=a&&tree[k].r<=b)//当前区间全部对要修改的区间有用 
    {
        tree[k].w+=(tree[k].r-tree[k].l+1)*x;//(r-1)+1区间点的总数
        tree[k].f+=x;
        return;
    }
    if(tree[k].f) down(k);//懒标记下传。只有不满足上面的if条件才执行,所以一定会用到当前节点的子节点 
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) add(k*2);
    if(b>m) add(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;//更改区间状态 
}

完整的代码:

#include<cstdio>
using namespace std;
int n,p,a,b,m,x,y,ans;
struct node
{
    int l,r,w,f;
}tree[400001];
inline void build(int k,int ll,int rr)//建树 
{
    tree[k].l=ll,tree[k].r=rr;
    if(tree[k].l==tree[k].r)
    {
        scanf("%d",&tree[k].w);
        return;
    }
    int m=(ll+rr)/2;
    build(k*2,ll,m);
    build(k*2+1,m+1,rr);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
inline void down(int k)//标记下传 
{
    tree[k*2].f+=tree[k].f;
    tree[k*2+1].f+=tree[k].f;
    tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
    tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
    tree[k].f=0;
}
inline void ask_point(int k)//单点查询
{
    if(tree[k].l==tree[k].r)
    {
        ans=tree[k].w;
        return ;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) ask_point(k*2);
    else ask_point(k*2+1);
}
inline void change_point(int k)//单点修改 
{
    if(tree[k].l==tree[k].r)
    {
        tree[k].w+=y;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) change_point(k*2);
    else change_point(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w; 
}
inline void ask_interval(int k)//区间查询 
{
    if(tree[k].l>=a&&tree[k].r<=b) 
    {
        ans+=tree[k].w;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) ask_interval(k*2);
    if(b>m) ask_interval(k*2+1);
}
inline void change_interval(int k)//区间修改 
{
    if(tree[k].l>=a&&tree[k].r<=b)
    {
        tree[k].w+=(tree[k].r-tree[k].l+1)*y;
        tree[k].f+=y;
        return;
    }
    if(tree[k].f) down(k);
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) change_interval(k*2);
    if(b>m) change_interval(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
int main()
{
    scanf("%d",&n);//n个节点 
    build(1,1,n);//建树 
    scanf("%d",&m);//m种操作 
    for(int i=1;i<=m;i++)
    {
        scanf("%d",&p);
        ans=0;
        if(p==1)
        {
            scanf("%d",&x);
            ask_point(1);//单点查询,输出第x个数 
            printf("%d",ans);
        } 
        else if(p==2)
        {
            scanf("%d%d",&x,&y);
            change_point(1);//单点修改 
        }
        else if(p==3)
        {
            scanf("%d%d",&a,&b);//区间查询 
            ask_interval(1);
            printf("%d\n",ans);
        }
        else
        {
             scanf("%d%d%d",&a,&b,&y);//区间修改 
             change_interval(1);
        }
    }
}

4、一些例题 

a)HDU1166 非常经典的题,可以用树状数组做,当然线段树入门的小白(like me 也可以用来练手。

主要用到单点修改和区间查询,所以不需要懒标记,主要代码非常模版,如下:

void build(int l,int r,int k)      ///建树
{
    tree[k].l=l;
    tree[k].r=r;
    if(l==r)
    {
      scanf("%d",&tree[k].w);
        return ;
    }
    int m=(l+r)/2;
    build(l,m,k*2);
    build(m+1,r,k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
void add(int k)                  ///单点修改
{
    if(tree[k].l==tree[k].r)
    {
        tree[k].w+=y;
        return;
    }
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) add(k*2);
    else add(k*2+1);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
inline void ask_interval(int k)      ///区间查询
{
    if(tree[k].l>=a&&tree[k].r<=b)
    {
        ans+=tree[k].w;
        return;
    }
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) ask_interval(k*2);
    if(b>m) ask_interval(k*2+1);
}

b)HDU 1754  维护区间最大值+单点修改 AC代码:

void build(int l,int r,int k)
{
    tree[k].l=l;
    tree[k].r=r;
    if(l==r)
    {
        scanf("%d",&tree[k].w);
        return ;
    }
    int m=(l+r)/2;
    build(l,m,k*2);
    build(m+1,r,k*2+1);
    tree[k].w=max(tree[k*2].w,tree[k*2+1].w);
}
void add(int k)
{
    if(tree[k].l==tree[k].r)
    {
        tree[k].w=y;
        return;
    }
    int m=(tree[k].l+tree[k].r)/2;
    if(x<=m) add(k*2);
    else add(k*2+1);
    tree[k].w=max(tree[k*2].w,tree[k*2+1].w);
}
inline void ask_interval(int k)
{
    if(tree[k].l>=a&&tree[k].r<=b)
    {
        ans=max(ans,tree[k].w);
        return;
    }
    int m=(tree[k].l+tree[k].r)/2;
    if(a<=m) ask_interval(k*2);
    if(b>m) ask_interval(k*2+1);
}

借鉴:https://www.cnblogs.com/TheRoadToTheGold/p/6254255.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值