线 段 树 : {\color{Red} 线段树:} 线段树:
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 O ( l o g N ) O(logN) O(logN)。而未优化的空间复杂度为 2 N 2N 2N,实际应用时一般还要开 4 N 4N 4N的数组以免越界,因此有时需要离散化让空间压缩。
定 义 : {\color{Red}定义:} 定义:
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
对于线段树中的每一个非叶子节点 [ a , b ] [a,b] [a,b],它的左儿子表示的区间为 [ a , ( a + b ) / 2 ] [a,(a+b)/2] [a,(a+b)/2],右儿子表示的区间为 [ ( a + b ) / 2 + 1 , b ] [(a+b)/2+1,b] [(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为 N N N,即整个线段区间的长度。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 O ( l o g N ) O(logN) O(logN)。而未优化的空间复杂度为 2 N 2N 2N,因此有时需要离散化让空间压缩。
基 本 结 构 : {\color{Red}基本结构:} 基本结构:
线段树是建立在线段的基础上,每个结点都代表了一条线段
[
a
,
b
]
[a,b]
[a,b]。长度为
1
1
1的线段称为元线段。非元线段都有两个子结点,左结点代表的线段为
[
a
,
(
a
+
b
)
/
2
]
[a,(a + b) / 2]
[a,(a+b)/2],右结点代表的线段为
[
(
(
a
+
b
)
/
2
)
+
1
,
b
]
[((a + b) / 2)+1,b]
[((a+b)/2)+1,b]。
下图就是两棵长度范围为
[
1
,
5
]
[
1
,
10
]
[1,5][1,10]
[1,5][1,10]的线段树。
长度范围为 [ 1 , L ] [1,L] [1,L] 的一棵线段树的深度为 l o g ( L ) + 1 log (L) + 1 log(L)+1。这个显然,而且存储一棵线段树的空间复杂度为 O ( L ) O(L) O(L)。
线段树支持最基本的操作为插入和删除一条线段。下面以插入为例,详细叙述,删除类似。
将一条线段 [ a , b ] [a,b] [a,b] 插入到代表线段 [ l , r ] [l,r] [l,r]的结点 p p p中,如果 p p p不是元线段,那么令 m i d = ( l + r ) / 2 mid=(l+r)/2 mid=(l+r)/2。如果 b < m i d b<mid b<mid,那么将线段 [ a , b ] [a,b] [a,b] 也插入到 p p p的左儿子结点中,如果 a > m i d a>mid a>mid,那么将线段 [ a , b ] [a,b] [a,b] 也插入到 p p p的右儿子结点中。
插入(删除)操作的时间复杂度为 O ( l o g n ) O(logn) O(logn)。
实 际 应 用 : {\color{Red}实际应用:} 实际应用:
上面的都是些基本的线段树结构,但只有这些并不能做什么,就好比一个程序有输入没输出,根本没有任何用处。
最简单的应用就是记录线段是否被覆盖,随时查询当前被覆盖线段的总长度。那么此时可以在结点结构中加入一个变量 i n t int int c o u n t count count;代表当前结点代表的子树中被覆盖的线段长度和。这样就要在插入(删除)当中维护这个 c o u n t count count值,于是当前的覆盖总值就是根节点的 c o u n t count count值了。
另外也可以将 c o u n t count count换成 b o o l bool bool c o v e r cover cover;支持查找一个结点或线段是否被覆盖。
实际上,通过在结点上记录不同的数据,线段树还可以完成很多不同的任务。例如,如果每次插入操作是在一条线段上每个位置均加 k k k,而查询操作是计算一条线段上的总和,那么在结点上需要记录的值为 s u m sum sum。
这里会遇到一个问题:为了使所有 s u m sum sum值都保持正确,每一次插入操作可能要更新 O ( N ) O(N) O(N)个 s u m sum sum值,从而使时间复杂度退化为 O ( N ) O(N) O(N)。
解决方案
L a z y Lazy Lazy思想:对整个结点进行的操作,先在结点上做标记,而并非真正执行,直到根据查询操作的需要分成两部分。
根据 L a z y Lazy Lazy思想,我们可以在不代表原线段的结点上增加一个值toadd,即为对这个结点,留待以后执行的插入操作 k k k值的总和。对整个结点插入时,只更新 s u m sum sum和 t o a d d toadd toadd值而不向下进行,这样时间复杂度可证明为 O ( l o g N ) O(logN) O(logN)。
进行懒惰标记的初级代码
void Lazy(int o,int left)
{
if(toadd[o])
{
toadd[o<<1]+=toadd[o];
toadd[o<<1|1]+=toadd[o];
sum[o<<1]+=toadd[o]*(left-(left>>1));
sum[o<<1|1]+=toadd[o]*(left>>1);
toadd[o]=0;
}
}
对一个 t o a d d toadd toadd 值为 0 0 0的结点整个进行查询时,直接返回存储在其中的 s u m sum sum值;而若对 t o a d d toadd toadd不为 0 0 0的一部分进行查询,则要更新其左右子结点的 s u m sum sum值,然后把 t o a d d toadd toadd值传递下去,再对这个查询本身,左右子结点分别递归下去。时间复杂度也是 O ( n l o g N ) O(nlogN) O(nlogN)。
要想运用首先需要建树这是建树代码:
void build_tree(int left,int right,int o)//建树
{
if(left==right)
{
cin>>sum[o];
return ;
}
int mid=(left+right)>>1;
build_tree(left,mid,o<<1);
build_tree(mid+1,right,o<<1|1);
sum[o]=sum[o<<1]+sum[o<<1|1];
}
要想真正掌握还是需要做题了解;
下面看一道题
A Simple Problem with Integers POJ - 3468
题意:
给定 Q ( 1 ≤ Q ≤ 100 , 000 ) 个 数 A 1 , A 2 … A Q Q (1 ≤ Q ≤ 100,000)个数A1,A2 … AQ Q(1≤Q≤100,000)个数A1,A2…AQ,以及可能多次进行的两个操作:
- 对某个区间 A i … A j Ai … Aj Ai…Aj的每个数都加 n n n( n n n可变)
- 求某个区间 A i … A j Ai … Aj Ai…Aj的数的和
值得注意的是
-
.数据范围,保存数据必须用 l o n g long long l o n g long long保存(除了区间可以用整形,其他最后用64位);
-
.只存和,会导致每次加数的时候都要更新到叶子节点,速度太慢,这是必须要避免的。
操作代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
using namespace std;
const int inf=0x3f3f3f3f;
#define ll long long
#define mem(a,b) memset(a,b,sizeof(a))
const int N=400040;
ll sum[N],add[N];
void build_tree(int l,int r,int o)//建树
{
if(l==r)
{
cin>>sum[o];
return ;
}
int mid=(l+r)>>1;
build_tree(l,mid,o<<1);
build_tree(mid+1,r,o<<1|1);
sum[o]=sum[o<<1]+sum[o<<1|1];
}
void pushdown(int o,int l)
{
if(add[o])
{
add[o<<1]+=add[o];
add[o<<1|1]+=add[o];
sum[o<<1]+=add[o]*(l-(l>>1));
sum[o<<1|1]+=add[o]*(l>>1);
add[o]=0;
}
}
void update(int x,int y,int l,int r,int o,int c)
{
if(l>=x&&r<=y)
{
sum[o]+=c*(r-l+1);
add[o]+=c;
return ;
}
pushdown(o,r-l+1);
int mid=(r+l)>>1;
if(x<=mid)update(x,y,l,mid,o<<1,c);
if(y>mid)update(x,y,mid+1,r,o<<1|1,c);
sum[o]=sum[o<<1]+sum[o<<1|1];
}
ll query(int x,int y,int l,int r,int o)
{
if(l>=x&&r<=y)
return sum[o];
pushdown(o,r-l+1);
int mid=(l+r)>>1;
ll Sum=0;
if(x<=mid)Sum=query(x,y,l,mid,o<<1);
if(y>mid)Sum+=query(x,y,mid+1,r,o<<1|1);
return Sum;
}
int main()
{
int n,m;
while(cin>>n>>m)
{
mem(sum,0);
mem(add,0);
build_tree(1,n,1);
while(m--)
{
string str;
int l,r,c;
cin>>str;
if(str=="Q")
{
cin>>l>>r;
cout<<query(l,r,1,n,1)<<endl;
}
else
{
cin>>l>>r>>c;
update(l,r,1,n,1,c);
}
}
}
return 0;
}