线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
定义:
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩。基本结构:
线段树是建立在线段的基础上,每个结点都代表了一条线段[a,b]。长度为1的线段称为元线段。非元线段都有两个子结点,左结点代表的线段为[a,(a + b) / 2],右结点代表的线段为[((a + b) / 2)+1,b]。
下图是长度范围为[1,10]的线段树。
![]()
长度范围为[1,L] 的一棵线段树的深度为log (L) + 1。这个显然,而且存储一棵线段树的空间复杂度为O(L)。
线段树支持最基本的操作为插入和删除一条线段。下面以插入为例,详细叙述,删除类似。
将一条线段[a,b] 插入到代表线段[l,r]的结点p中,如果p不是原线段,那么令mid=(l+r)/2。如果b<mid,那么将线段[a,b] 也插入到p的左儿子结点中,如果a>mid,那么将线段[a,b] 也插入到p的右儿子结点中。插入(删除)操作的时间复杂度为O(logn)。
理解:
线段树上的每一个节点T[a , b],代表该节点维护了原数列[ a , b ]区间的信息。
对于每一个节点他至少有三个信息:左端点,右端点,我们需要维护的信息(在本题中我们维护区间和)。
由于线段树是一个二叉树,而且是一个平衡二叉树,如果当前结点的编号是i,左端点为L ,右端点为 R , 那么左儿子的 编号为 i*2 ,左端点为 L ,右端点为 (L + R)/2 ; 同理右儿子的 编号为 i*2+1,左端点为(L+R)/2 ,右端点为 R。
如果当前结点的左端点等于右端点,那么该节点就是叶子节点,直接在该节点赋值即可。显然线段树是递归定义的。
线段树就是这样一种数据结构,讲一个大区间分为若干个不相交的区间,每次维护都在小区间上处理,并且查询也在这些被分解的区间中信息合并出我们需要的结果,这就是线段树高效的原因。
建树:
线段树的构建是自顶点而下,即从根节点开始递归构建,根据线段树定义,当左端点等于右端点时(达到递归边界),直接赋值即可,回溯时也要维护区间,代码如下:
void Build_Tree ( int x , int y , int i )
{
tr[i].l = x;
tr[i].r = y;
if( x == y )tr[i].sum = a[x] ; //找到叶子节点,赋值
else
{
ll mid = (tr[i].l tr[i].r ) >> 1 ;
Build_Tree ( x , mid , i << 1); //左子树
Build_Tree ( mid + 1 , y , i << 1 | 1); //右子树
tr[i].sum = tr[i << 1].sum + tr[i << 1 | 1].sum; //回溯维护区间和
}
}
维护树:
维护树的方法也很好理解,如果目标更新节点在左儿子里,去左儿子中查找;反之,在右儿子中。不断递归,知道找到需要维护的节点,更新它,回溯是一路更新回来。这就是维护的过程,代码如下:
void Update_Tree ( int q , int val , int i )
{
if(tr[i].l == q && tr[i].r == q) //找到需要修改的叶子节点
{
tr[i].sum = val ; //更新当前结点
}
else //当前结点是非叶子结点
{
long long mid = (tr[i].l tr[i].r ) >> 1 ; //取中间
if ( q <= mid ) //目标节点在左儿子中
{
Update_Tree ( q , val , i << 1 );
}
else if( q > mid ) //目标节点在右儿子中
{
Update_Tree ( q , val , i << 1 | 1 );
}
tr[i].sum = tr[i << 1].sum + tr[i << 1 | 1].sum; //回溯
}
}
查询树:
查询区间求和,不难想到如果当前结点的区间完全被目标区间包含,直接返回当前结点的sum值,否则分类讨论。具体过程通过以下代码理解:
long long Query_Tree ( int q , int w , int i )
{
if ( q <= tr[i].l && w >= tr[i].r ) return tr[i].sum; //当前结点的区间完全被目标区间包含
else
{
long long mid = (tr[i].l tr[i].r) >> 1;
if( q > mid ) //完全在左儿子
{
return Query_Tree ( q , w , i << 1 | 1);
}
else if (w <= mid ) //完全在右儿子
{
return Query_Tree ( q , w , i << 1);
}
else //目标区间在左右都有分布
{
return Query_Tree ( q , w , i << 1) + Query_Tree ( q , w , i << 1 | 1 );
}
}
}