什么是线段树
线段树,是一种二叉搜索树。它将一段区间划分为若干单位区间,每一个节点都储存着一个区间。它功能强大,支持区间求和,区间最大值,区间修改,单点修改等操作。
线段树的思想本质是一种分治思想。
线段树的每一个节点都储存着一段区间[L…R]的信息,其中叶子节点L=R。它的大致思想是:将一段大区间平均地划分成2个小区间,每一个小区间都再平均分成2个更小区间……以此类推,直到每一个区间的L等于R(这样这个区间仅包含一个节点的信息,无法被划分)。通过对这些区间进行修改、查询,来实现对大区间的修改、查询。
这样一来,每一次修改、查询的时间复杂度都只为O(log2n)。
但是,可以用线段树维护的问题必须满足区间加法,否则是不可能将大问题划分成子问题来解决的。
什么是区间加法
一个问题满足区间加法,仅当对于区间[L,R]的问题的答案可以由[L,M]和[M+1,R]的答案合并得到。
经典的区间加法问题有:
-
区间求和(∑Ri=Lai=∑Mi=Lai+∑Ri=M+1ai (L≤M<R))
-
区间最大值(maxRi=Lai=max(maxMi=Lai,maxRi=M+1ai) (L≤M<R))
不满足区间加法的问题有:
-
区间的众数
-
区间的最长不下降子序列
线段树的实现
数据结构的定义
要点分析
- 线段树是如上图所示的完全二叉树。其中根节点涵盖整个区间,其子节点涵盖左右各半的子区间,叶子结点为单个数据点。作为完全二叉树,如果定义根节点序号为1,则序号为p的父节点对应的左、右子节点的序号分别为2p、2p+1。实现时,可以用位操作优化。其中,p左移(<<)1位后,末尾为0,或(|)一个1相当于加(+)1。
- 注意,由于叶子结点(原始数据点)数量不一定为2的整数次幂,故线段树不一定为满二叉树。所以对应n个叶子结点,线段树的总结点数趋于4n。(满二叉树的总结点数为n+n/2+n/4+…+1=>2n,完全二叉树的节点占用序号显然更大,因为存在部分浪费,
但4n我不会算) - 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;}
线段树的构建
要点分析
- push_up()用于合并子区间的信息,针对不同的操作合并细节也有所不同。代码中以求和操作为例,
- 对于一棵树来说,其建立自然可以采取递归的方式进行。当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而已。所以并不需要单独编写代码。
实现代码
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);
}
区间更新
要点分析
- 区间更新即对指定范围[l…r]进行更新,但如果朴素地用多个单点更新,实现复杂度为nlogn,并没有利用到线段树树的优势。
- 对于单点修改,要修改所有的父区间,即要向上更新;但对于区间修改,由于修改的是某个区间,而线段树的节点也都是区间,所以修改不仅要推广到所有父区间,还有继承至所有子区间(点修改由于修改的是叶子节点,所以没有子区间),即要进行向上更新和向下更新。
- 由于父节点是对子节点信息的一个概括,所以当使用父节点时,子节点的信息是无关紧要的,因此子节点信息的更新可以延时到访问子节点时进行。为此,引入了lazy_tag(懒标记)。lazy_tag的含义为:当前节点的信息已经更新,但是子节点的信息尚需更新。
- 有时多次修改产生的标记可能会相互影响,所以在递归节点前,因现将 lazy_tag向下传递。传递包括tag的传递和节点信息的更新,这里用push_down()实现。
- 需要进行push_down()的情形有两种:
- 区间更递归前
- 区间查询递归前
- 区间更新时,仍然要在回溯时进行push_up()以完成向上更新。
- 当欲更新的区间[ul,ur]完全包含当前区间节点p时,此区间节点p因被立即更新;否则,应该去考虑其左右子区间(对应下图情形)
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;
}
参考博客
- https://www.cnblogs.com/huangzihaoal/p/11161024.html
- https://www.cnblogs.com/iris001999/articles/9058603.html
- https://www.luogu.org/problemnew/solution/P3372