对线段树的理解与探究

什么是线段树

线段树,是一种二叉搜索树。它将一段区间划分为若干单位区间,每一个节点都储存着一个区间。它功能强大,支持区间求和区间最大值区间修改单点修改等操作。

线段树的思想本质是一种分治思想

线段树的每一个节点都储存着一段区间[L…R]的信息,其中叶子节点L=R。它的大致思想是:将一段大区间平均地划分成2个小区间,每一个小区间都再平均分成2个更小区间……以此类推,直到每一个区间的L等于R(这样这个区间仅包含一个节点的信息,无法被划分)。通过对这些区间进行修改、查询,来实现对大区间的修改、查询。

这样一来,每一次修改查询的时间复杂度都只为O(log2n)

但是,可以用线段树维护的问题必须满足区间加法,否则是不可能将大问题划分成子问题来解决的。

什么是区间加法

一个问题满足区间加法,仅当对于区间[L,R]的问题的答案可以由[L,M]和[M+1,R]的答案合并得到

经典的区间加法问题有:
  1. 区间求和(∑Ri=Lai=∑Mi=Lai+∑Ri=M+1ai (L≤M<R))

  2. 区间最大值(maxRi=Lai=max(maxMi=Lai,maxRi=M+1ai) (L≤M<R))

不满足区间加法的问题有:
  1. 区间的众数

  2. 区间的最长不下降子序列

线段树的实现

数据结构的定义

image

要点分析
  1. 线段树是如上图所示的完全二叉树。其中根节点涵盖整个区间,其子节点涵盖左右各半的子区间,叶子结点为单个数据点。作为完全二叉树,如果定义根节点序号为1,则序号为p的父节点对应的左、右子节点的序号分别为2p2p+1。实现时,可以用位操作优化。其中,p左移(<<)1位后,末尾为0,或(|)一个1相当于加(+)1
  2. 注意,由于叶子结点(原始数据点)数量不一定为2的整数次幂,故线段树不一定为满二叉树。所以对应n个叶子结点,线段树的总结点数趋于4n。(满二叉树的总结点数为n+n/2+n/4+…+1=>2n,完全二叉树的节点占用序号显然更大,因为存在部分浪费但4n我不会算
  3. tag是懒惰标记,用于优化区间修改,后面详述,这里不谈。
实现代码
typedef long long ll;

const ll maxn=1e5+10;

int n,m;
ll a[maxn],t[maxn*4],tag[maxn*4];
inline int lc(int p){return p<<1;}
inline int rc(int p){return p<<1|1;}

线段树的构建

要点分析
  1. push_up()用于合并子区间的信息针对不同的操作合并细节也有所不同。代码中以求和操作为例,
  2. 对于一棵来说,其建立自然可以采取递归的方式进行。当l==r时为叶子结点,直接写入原始数据。push_up()应放在递归完成后(此时子节点信息完成)的回溯部分进行。
实现代码
inline void push_up(int p){t[p]=t[lc(p)]+t[rc(p)];}

void build(int p,int l,int r)
{
    tag[p]=0;
    if(l==r){t[p]=a[l];return;}
    int mid=(l+r)>>1;
    build(lc(p),l,mid);
    build(rc(p),mid+1,r);
    push_up(p);
}

单点更新

要点分析
  1. 单点更新的思路同二叉搜索树一致,找到叶子结点更新。不同的是,更新叶子后,回溯时需要更新对应的祖先节点
  2. 单点更新其实是一种特殊的区间更新,只是区间长度为1而已。所以并不需要单独编写代码
实现代码
void update(int up,ll k,int p,int l,int r)
{
    if(l==r){t[p]=k;return;}
    int mid=(l+r)>>1;
    if(up<=mid)
        update(up,k,lc(p),l,mid);
    else
        update(up,k,rc(p),mid+1,r);
    push_up(p);
}

区间更新

要点分析
  1. 区间更新即对指定范围[l…r]进行更新,但如果朴素地用多个单点更新,实现复杂度为nlogn,并没有利用到线段树树的优势
  2. 对于单点修改,要修改所有的父区间,即要向上更新;但对于区间修改,由于修改的是某个区间,而线段树的节点也都是区间,所以修改不仅要推广到所有父区间,还有继承至所有子区间点修改由于修改的是叶子节点,所以没有子区间),即要进行向上更新向下更新
  3. 由于父节点是对子节点信息的一个概括,所以当使用父节点时,子节点的信息是无关紧要的,因此子节点信息的更新可以延时访问子节点时进行。为此,引入了lazy_tag(懒标记)。lazy_tag的含义为:当前节点的信息已经更新,但是子节点的信息尚需更新
  4. 有时多次修改产生的标记可能会相互影响,所以在递归节点前,因现将 lazy_tag向下传递。传递包括tag的传递节点信息的更新,这里用push_down()实现。
  5. 需要进行push_down()的情形有两种:
    1. 区间更递归前
    2. 区间查询递归前
  6. 区间更新时,仍然要在回溯时进行push_up()以完成向上更新
  7. 当欲更新的区间[ul,ur]完全包含当前区间节点p时,此区间节点p因被立即更新;否则,应该去考虑其左右子区间(对应下图情形)
    image
if(ul<=mid)
    update(ul,mid,lc(p),l,mid);
if(ur>mid)//ur>=mid+1
    update(mid+1,ur,rc(p),mid+1,r);

如上述代码,当前区间和修改区间都被切成左右两部分。但在实现时,修改区间按原来的[ul,ur]来写也没有问题,因为多余的部分不会影响包含性的判断。例如,总区间为[1…5],现在将[1…10]的值都加2,这和对区间[1…5]加2是没有区别的。

实现代码
inline void __update(int p,int l,int r,ll k){tag[p]+=k,t[p]+=k*(r+1-l);}

void push_down(int p,int l,int r)
{
    if(!tag[p])
        return;
    int mid=(l+r)>>1;
    __update(lc(p),l,mid,tag[p]);
    __update(rc(p),mid+1,r,tag[p]);
    tag[p]=0;
}

void update(int ul,int ur,ll k,int p,int l,int r)
{
    if(ul<=l && r<=ur){__update(p,l,r,k);return;}
    push_down(p,l,r);
    int mid=(l+r)>>1;
    if(ul<=mid){update(ul,ur,k,lc(p),l,mid);}
    if(ur>mid){update(ul,ur,k,rc(p),mid+1,r);}
    push_up(p);
}

区间查询

要点分析

区间查询的原理同区间更新一致,不再赘述。

实现代码
ll query(int ql,int qr,int p,int l,int r)
{
    ll ans=0;
    if(ql<=l && r<=qr){return t[p];}
    push_down(p,l,r);
    int mid=(l+r)>>1;
    if(ql<=mid){ans+=query(ql,qr,lc(p),l,mid);}
    if(qr>mid){ans+=query(ql,qr,rc(p),mid+1,r);}
    return ans;
}

参考博客

  1. https://www.cnblogs.com/huangzihaoal/p/11161024.html
  2. https://www.cnblogs.com/iris001999/articles/9058603.html
  3. https://www.luogu.org/problemnew/solution/P3372
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值