线段树(Segmen Tree)是一种基于分治思想的二叉树结构,用于在区间上进行信息统计。与按照二进制位(2的次幂)进行区间划分的树状数组相比,线段树是一种更加通用的结构。
1、线段树的每个节点都代表一个区间。
2、线段树具有唯一的根节点,代表的区间是整个统计范围[1,N]。
3、线段树的每个叶子节点都代表一个长度为1的元区间[x,x]。
4、对于每个内部节点[l,r],它的左子节点是[l,mid],右子节点是[mid+1,r](向下取整)。
区间视角,如下图:
二叉树视角,如下图:
上图展示了一棵线段树。可以发现,除去树的最后一层,整棵线段树一定是一棵完全二叉树,树的深度为O(logN)。因此,我们可以按照与二叉堆类似的“父子2倍”节点编号方法。
1、根节点的编号为1。
2、编号为x的节点的左子节点编号为 x * 2,右子节点编号为 x*2+1.
这样一来,我们就能简单的使用一个struct数组来保存线段树。当然,树的最后一层节点在数组中保存的位置不是连续的,直接空出多余的位置即可。
在理想情况下,N个数的序列,构建线段树后,这N个数都是叶子节点,N个叶子节点的满二叉树有N+N/2+N/4+N/8+…+2+1个节点。这是从叶子节点一直加到根节点。
上述存储方式下,最后一层产生了空余,所以保存线段树的数组长度不小于4N,才能保证不会越界。
线段树的建树
线段树的基本用途是对序列进行维护,支持查询与修改指令。给定一个长度为N的序列A,我们可以在[1,N]上建立一棵线段树,每个叶子节点[i,i],保存A[i]的值。线段树的二叉树结构可以方便地从下到上传递信息。
以区间最大值为例,记为dat(l,r)等于dat(l,r)=max(dat(l,mid),dat(mid+1,r)).
如下图:
下面代码建立了一棵线段树并在每个节点上保存了对应区间的最大值。
#include
#include
#include
using namespace std;
const int SIZE=10;
int a[SIZE+1]={0,3,6,4,8,1,2,9,5,7,0};
struct SegmentTree{
int l,r;
int dat;
}t[SIZE*4];
void build(int p,int l,int r){
t[p].l=l,t[p].r=r;
if(l==r) {t[p].dat=a[l];return ;}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
t[p].dat=max(t[2*p].dat,t[2*p+1].dat);
}
int main()
{
int n=SIZE;
build(1,1,n);//调用入口
cout<<t[10].dat<<endl;
cout<<t[1].dat<<endl;t[1],就是根节点,表示最大区间
return 0;
}
如上图,t[10]这个表示第10个节点,第10个节点的区间是[4,4],最大是dat=8。
线段树的单点修改
单点修改是一条形如“C x v"的指令,表示把A[x]的值修改为v。
在线段树中,根节点(编号为1的节点)是执行各种指令的入口。我们需要从根节点出发,递归找到代表区间[x,x]的叶节点,然后从下到上更新[x,x]以及它的所有祖先节点上保存的信息。时间复杂度为O(log N)。
看下图:
在上面的代码加入单点修改函数 void change(int p,int x,int v)
#include
#include
#include
using namespace std;
const int SIZE=10;
int a[SIZE+1]={0,3,30,4,8,1,2,9,5,7,0};
struct SegmentTree{
int l,r;
int dat;
}t[SIZE*4];
void build(int p,int l,int r){
t[p].l=l,t[p].r=r;
if(l==r) {t[p].dat=a[l];return ;}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
t[p].dat=max(t[2*p].dat,t[2*p+1].dat);
}
void change(int p,int x,int v)
{
if(t[p].l==t[p].r){
t[p].dat=v;
return;
}
int mid=(t[p].l+t[p].r)/2;
//cout<
if(x<=mid) change(p*2,x,v);
else change(p*2+1,x,v);
t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
}
int main()
{
int n=SIZE;
build(1,1,n);//调用入口
cout<<t[10].dat<<endl;
cout<<t[1].dat<<endl;//t[1],就是根节点,表示最大区间
change(1,7,50);
cout<<t[1].dat<<endl;
return 0;
}
线段树的区间查询
区间查询是一条形如“Q l r”的指令,查询序列A在区间[l,r]上的最大值,即max{A[i]} l<=i<=r.
我们只需要从根节点开始,递归执行以下过程:
1、若[l,r]完全覆盖了当前节点代表的区间,则立即回溯,并且该节点的dat值为候选答案。
2、若左子节点与[l,r]有重叠部分,则递归访问左子节点。
3、若右子节点与[l,r]有重叠部分,则递归访问右子节点。
在上面代码的基础上加入了 ask(int p,int l,int r)函数
#include
#include
#include
using namespace std;
const int SIZE=10;
int a[SIZE+1]={0,3,30,4,8,1,2,9,5,7,0};
struct SegmentTree{
int l,r;
int dat;
}t[SIZE*4];
void build(int p,int l,int r){
t[p].l=l,t[p].r=r;
if(l==r) {t[p].dat=a[l];return ;}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
t[p].dat=max(t[2*p].dat,t[2*p+1].dat);
}
int ask(int p,int l,int r)
{
if(l<=t[p].l&&r>=t[p].r) return t[p].dat;
int mid=(t[p].l+t[p].r)>>1;
int val=-(1<<30);
if(l<=mid) val=max(val,ask(p*2,l,r));
if(r>mid) val=max(val,ask(p*2+1,l,r));
return val;
}
void change(int p,int x,int v)
{
if(t[p].l==t[p].r){
t[p].dat=v;
return;
}
int mid=(t[p].l+t[p].r)/2;
//cout<
if(x<=mid) change(p*2,x,v);
else change(p*2+1,x,v);
t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
}
int main()
{
int n=SIZE;
build(1,1,n);//调用入口
cout<<t[10].dat<<endl;
cout<<t[1].dat<<endl;//t[1],就是根节点,表示最大区间
change(1,7,50);
cout<<t[1].dat<<endl;
cout<<ask(1,1,5)<<endl;
cout<<ask(1,6,10)<<endl;
return 0;
}
总结一下线段树的区间查询:
该查询过程会把询问区间[l,r]在线段上分成O(logN)个节点,取它们的最大值作为答案。
为什么是O(logN)个呢?
我们通过上图分析一下过程,在每个节点[pl , pr]上,
设mid=(pl +pr)/2;
可能出现以下几种情况:
1、该节点在区间[l , r]内,也就是l<=pl<=pr<=r,直接返回值,当成候选值。
2、只有左区间l落在节点区间内,也就是pl<=l<=pr<=r,也不知道l是在节点区间的左半区间,还是右半区间,我问分类讨论:
(1)、l>mid,只会递归右子树
(2)、l<=mid,只会递归左子树
3、只有r落在节点区间内,也就是l<=pl<=r<=pr,与情况2类似。
4、区间[l,r]落在节点区间内,即pl<=l<=r<=pr
(1)、l,r多为与mid的一侧,只递归一棵子树。
(2)、l,r分别位于mid的两侧,递归左右两个子树。
也就说是4(2)这种情况至多发生一次,之后子节点就会变成2或3。
上述过程时间复杂度为O(2logN)=O(log N)。