线段树定义
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
——百度百科
线段树的基本结构:
若侵请提醒删
个人总结
1.线段树是一种二叉搜索树,可以存储并维护区间信息。
2.核心思想:二分,递归。
3.基本操作:
①查询
②数据更新(单点更新,区间更新)
4.建议开4N的空间。
5.建议区间都以闭区间为标准,起始建议从1开始。(别给自己找麻烦)
可以清楚得看到每个结点存储了所维护的区间以及区间的某些信息。以最简单的线段树为例,每个结点仅存储了所维护的区间和其最大值或者最小值,这里以给出一道裸题为例(记录了区间的最大值):http://acm.hdu.edu.cn/showproblem.php?pid=1754
最简单的线段树+单点跟新
//原始线段树 裸题:hdu1754
#include<iostream>
#include<string>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N=200010;
struct node{
int left;//左区间
int right;//右区间
int val;//所需要的信息
}tree[4*N];
int arr[N];
//root:1~n,arr:1~n
int Max(int a,int b){
return a>b?a:b;
}
/*********************
*建树操作:
*arr数组存储了原始数据
*利用递归,完成建树
*假设父结点:root 所维护的区间:[l,r]
*则孩子结点:mid=(l+r)/2
* 左孩子:root*2 所维护的区间:[l,mid]
* 右孩子:root*2+1 所维护的区间:[mid+1,r]
* 递归基:当l==r时,即所维护的区间是一点时
* 此时,可以为结点赋值:tree[root].val=arr[l];
*
*********************/
void build(int root,int l,int r){//root父亲结点下标
tree[root].left=l;//存储区间
tree[root].right=r;
if(l==r){//递归基
tree[root].val=arr[l];
return ;
}
int mid=(l+r)>>1;
//递归建树
build(root<<1,l,mid);
build((root<<1)|1,mid+1,r);
//回溯,向上更新区间最大值
tree[root].val=Max(tree[root<<1].val,tree[(root<<1)|1].val);
}
//查询
int query(int root,int l,int r){//l,r:需要查询的区间为[l,r]
int ll=tree[root].left;
int rr=tree[root].right;
if(ll==l && rr==r)//所需查询的区间恰好符合时,直接返回此节点的值即可
return tree[root].val;
//其实以下代码写的过于复杂了,可以简化,但原理一样 简化操作参见下面的区间更新的查询操作
int mid=(ll+rr)>>1;
if(mid>=r)//所需查询的区间完全在左孩子的区间中
query(root<<1,l,r);
else if(mid+1<=l)//完全在右孩子的区间中
query((root<<1)|1,l,r);
else//左右孩子均存在
return Max(query(root<<1,l,mid),query((root<<1)|1,mid+1,r));
}
//单点更新
//其实只需利用递归更新所有父节点即可
void updateOne(int root,int index,int val){
int ll=tree[root].left;
int rr=tree[root].right;
if(ll==rr){
if(index==ll){
tree[root].val=val;//找到此节点,更新值
return ;
}
}
int mid=(ll+rr)>>1;
if(index <= mid)//继续向下寻找结点
updateOne(root<<1,index,val);
else
updateOne((root<<1)|1,index,val);
//回溯更新父节点
tree[root].val=Max(tree[root].val,val);
}
int main()
{
int n,m;
string str;
int x,y;
while(cin >> n >> m){
for(int i=1;i<=n;i++)
scanf("%d",&arr[i]);
build(1,1,n);
while(m--){
cin >> str >> x >> y;
if(str[0]=='Q'){
printf("%d\n",query(1,x,y));
}else if(str[0]=='U'){
updateOne(1,x,y);
}else{}
}
}
return 0;
}
稍复杂的线段树+区间跟新
同样给出例题:https://www.luogu.org/problemnew/show/P3372
此题是求区间和。因此:父亲的值=左孩子的值+右孩子的值
相比于单点更新,区间跟新引入了延迟标记的概念。
延迟标记(lazy):首先:我们并不急着一次性更新所有点的值,而只是记在区间上——假如[1,10]每个元素都加了10,那么我们只需将10记在根节点上即可,那么此时整个区间的和=原此结点的值+区间元素个数*10。其次:我们知道,查询操作和区间跟新都可能会向下深入(注:并不是所有都会深入到叶子节点才结束,易证),我们就借用他们向下深入的特性来传递lazy值。
//区间更新的线段树
typedef long long ll;
const int maxn=100005;
int a[maxn];
struct node{
int left,right;
ll sum,lazy;
void update(ll val){//结点内部跟新,别和下面区间跟新混淆
sum+=(right-left+1)*val;
lazy+=val;
}
}s[4*maxn];
void p_up(int k){ //更新k节点存储的sum
s[k].sum=s[2*k].sum+s[2*k+1].sum;
return ;
}
void p_down(int k){//更新k点的lazy,将lazy传递给孩子
ll val=s[k].lazy;
if(val){//若右lazy传递下来,则让孩子跟新
s[2*k].update(val);
s[2*k+1].update(val);
s[k].lazy=0;//传递完之后,自身的lazy清零
}
}
void build(int l,int r,int k){
s[k].left=l;
s[k].right=r;
s[k].lazy=0;//其实lazy自然为0
if(l==r){
s[k].sum=a[l];
return ;
}
int mid=(l+r)/2;
build(l,mid,2*k);
build(mid+1,r,2*k+1);
p_up(k);//跟新自身结点的值
}
void update(int l,int r,int k,ll val){
if(l<=s[k].left && s[k].right<=r){//简化:当结点维护区间小于等于查询区间时即可跟新结点
s[k].update(val);
return ;
}
p_down(k);//向下传递lazy
int mid=(s[k].left+s[k].right)/2;
if(l<=mid) update(l,r,2*k,val);
if(r>mid) update(l,r,2*k+1,val);
p_up(k);//跟新自身的值
}
ll fin(int l,int r,int k){//查找区间[l,r]的和
if(l<=s[k].left && s[k].right<=r)
return s[k].sum;
ll sum=0;
p_down(k);
int mid=(s[k].left+s[k].right)/2;
if(l<=mid) sum+=fin(l,r,2*k);
if(r>mid) sum+=fin(l,r,2*k+1);
p_up(k);
return sum;
}
这篇写的相比之前简略了不少,如有不足,希望指出,谢谢。