线段树详解
参考 http://blog.csdn.net/metalseed/article/details/8039326
一:线段树基本概念
1:概述
线段树,类似区间树,是一个完全二叉树,它在各个节点保存一条线段(数组中的一段子数组),主要用于高效解决
连续区间的动态查询问题,由于二叉结构的特性,它基本能保持每个操作的复杂度为O(lgN)!
性质:父亲的区间是[a,b],(c=(a+b)/2)左儿子的区间是[a,c],右儿子的区间是[c+1,b],线段树需要的空间为数组大小的四倍
2:基本操作(demo用的是查询区间最小值)
线段树的主要操作有:
(1):线段树的构造 void buildtree(int node,int l,int r);
主要思想是递归构造,如果当前节点记录的区间只有一个值,则直接赋值,否则递归构造左右子树,
最后回溯的时候给当前节点赋值
#include <iostream> #include <cstdio> #include <cstring> #include <string> #include <queue> #include <cmath> #include <algorithm> using namespace std;
#define ls node<<1,l,m
#define rs node<<1|1,m+1,r const int maxn=256; int Tree[maxn*4];void buildtree(int node,int l,int r) { if(l==r) scanf("%d",&Tree[node]);/*只有一个元素,节点记录该单元素*/ else { /*递归构造左右子树*/buildtree(ls);
buildtree(rs); /*回溯时得到当前node节点的线段信息*/ Tree[node]=Tree[node<<1]+Tree[node<<1|1]; } }
(2):区间查询int query(int node,int l,int r,int L,int R);
(其中node为当前查询节点,l,r为当前节点存储的区间,L,R为此次query所要查询的区间)
主要思想是把所要查询的区间[a,b]划分为线段树上的节点,然后将这些节点代表的区间合并起来得到所需信息
int query(int node,int l,int r,int L,int R)
{
if(L<=l&&r<=R) return Tree[node];
int m=(l+r)>>1;
int ans=0;
if(m>=L) ans+=query(ls,L,R);
if(m<R) ans+=query(rs,L,R);
return ans;
}
可见,这样的过程一定选出了尽量少的区间,它们相连后正好涵盖了整个[L,R],没有重复也没有遗漏。同时,考虑到线段树上每层的节点最多会被选取2个,一共选取的节点数也是O(log n)的,
因此查询的时间复杂度也是O(log n)。
线段树并不适合所有区间查询情况,它的使用条件是“相邻的区间的信息可以被合并成两个区间的并区间的信息”。即问题是可以被分解解决的。
(3):区间或节点的更新及线段树的动态维护update(这是线段树核心价值所在,节点中的标记域可以解决N多种问题)
动态维护需要用到标记域,延迟标记等。
a:单节点更新
void update(int node,int l,int r,int ind,int add) { if(l==r)
{
Tree[node]+=add;
return ;
}
int m=(l+r)>>1;
if(ind<=m) update(ls,ind,add);
else update(rs,ind,add);
/*回溯更新父节点*/
Tree[node]+=add; }
b:区间更新(线段树中最有用的)
需要用到延迟标记,每个结点新增加一个标记,记录这个结点是否被进行了某种修改操作(这种修改操作会影响其子结点)。对于任意区间的修改,我们先按照查询的方式将其划分成线段树中的结点,然后修改这些结点的信息,并给这些结点标上代表这种修改操作的标记。在修改和查询的时候,如果我们到了一个结点p,并且决定考虑其子结点,那么我们就要看看结点p有没有标记,如果有,就要按照标记修改其子结点的信息,并且给子结点都标上相同的标记,同时消掉p的标记(优点在于,不用将区间内的所有值都暴力更新,大大提高效率,因此区间更新是最有用的操作)
void change(node *p,int a,int b) { if(a<=p->left&&p->right<=b)//当前节点的区间包含在修改区间内 { ...... return ; } Push_Down(p);//把当前节点的标记向下传递 int mid=(p->left+p->right)/2;//计算左右子节点的分隔点 if(a<mid) change(p->lch,a,b);//和左孩子有交集,考察左子节点 if(b>mid) change(p->rch,a,b);//和右孩子有交集,考察右子节点 update(p); }
3:主要应用
(1):区间最值查询问题 (见模板1)
(2):连续区间修改或者单节点更新的动态查询问题 (见模板2)
二:典型模板
模板1:
RMQ,查询区间最值下标---min(可用ST算法)
#include <iostream> #include <cstdio> #include <cstring> #include <string> #include <queue> #include <cmath> #include <algorithm> using namespace std; #define MAXN 100 #define MAXIND 256//线段树节点个数 //构建线段树,目的:得到M数组. void build(int node,int b,int e,int M[],int A[]) { if(b==e) M[node]=b;//只有一个元素,只有一个下标 else { build(2*node,b,(b+e)/2,M,A); build(2*node+1,(b+e)/2+1,e,M,A); if(A[M[2*node]]<=A[M[2*node+1]]) M[node]=M[2*node]; else M[node]=M[2*node+1]; } } //找出区间 [i, j] 上的最小值的索引 int query(int node,int b,int e,int M[],int A[],int i,int j) { int p1,p2; //查询区间和要求的区间没有交集 if(i>e||j<b) return -1; if(b>=i&&e<=j) return M[node]; p1=query(2*node,b,(b+e)/2,M,A,i,j); p2=query(2*node+1,(b+e)/2+1,e,M,A,i,j); if(p1==-1) return M[node]=p2; if(p2==-1) return M[node]=p1; if(A[p1]<=A[p2]) return M[node]=p1; return M[node]=p2; } int main() { int M[MAXIND];//下标1起才有意义,否则不是二叉树,保存下标编号节点对应区间最小值的下标. memset(M,-1,sizeof(M)); int a[]={3,4,5,7,2,1,0,3,4,5}; build(1,0,sizeof(a)/sizeof(a[0])-1,M,a); cout<<query(1,0,sizeof(a)/sizeof(a[0])-1,M,a,0,5)<<endl; return 0; }
模板2:
连续区间修改或者单节点更新的动态查询问题 (此模板查询区间和)(可用树状数组更简洁)
#include <iostream> #include <cstdio> #include <cstring> #include <string> #include <queue> #include <cmath> #include <algorithm> using namespace std; #define lson l,m,rt<<1 #define rson m+1,r,rt<<1|1 #define root 1,N,1 #define LL long long const int maxn=111111; LL add[maxn<<2]; LL sum[maxn<<2]; void PushUp(int rt) { sum[rt]=sum[rt<<1]+sum[rt<<1|1]; } void PushDown(int rt,int m) { if (add[rt]) { add[rt<<1]+=add[rt]; add[rt<<1|1]+=add[rt]; sum[rt<<1]+=add[rt]*(m-(m>>1)); sum[rt<<1|1]+=add[rt]*(m>>1); add[rt]=0; } } void build(int l,int r,int rt) { add[rt]=0; if (l==r) { scanf("%lld",&sum[rt]); return ; } int m=(l+r)>>1; build(lson); build(rson); PushUp(rt); } void update(int L,int R,int c,int l,int r,int rt) { if (L<=l&&r<=R) { add[rt]+=c; sum[rt]+=(LL)c*(r-l+1); return ; } PushDown(rt,r-l+1); int m=(l+r)>>1; if (L<=m) update(L,R,c,lson); if (m<R) update(L,R,c,rson); PushUp(rt); } LL query(int L,int R,int l,int r,int rt) { if (L<=l&&r<=R) return sum[rt]; PushDown(rt,r-l+1); int m=(l+r)>>1; LL ret=0; if (L<=m) ret+=query(L,R,lson); if (m<R) ret+=query(L,R,rson); return ret; } int main() { int N,Q; scanf("%d%d",&N,&Q); build(root); while (Q--) { char op[2]; int a,b,c; scanf("%s",op); if (op[0]=='Q') { scanf("%d%d",&a,&b); printf("%lld\n",query(a,b,root)); } else { scanf("%d%d%d",&a,&b,&c); update(a,b,c,root); } } return 0; }