数据结构之线段树 单点更新

来源:互联网 发布:淘宝客服介入 编辑:程序博客网 时间:2024/05/17 16:43

http://dongxicheng.org/structure/segment-tree/

http://acm.hdu.edu.cn/showproblem.php?pid=1166

http://www.2cto.com/kf/201207/141870.html

1、概述

线段树,也叫区间树,是一个完全二叉树,它在各个节点保存一条线段(即“子数组”),因而常用于解决数列维护问题,它基本能保证每个操作的复杂度为O(lgN)。

2、线段树基本操作

线段树的基本操作主要包括构造线段树,区间查询和区间修改。

(1)    线段树构造

首先介绍构造线段树的方法:让根节点表示区间[0,N-1],即所有N个数所组成的一个区间,然后,把区间分成两半,分别由左右子树表示。不难证明,这样的线段树的节点数只有2N-1个,是O(N)级别的,如图:(注意:线段树并非是一颗完全二叉树,所以其所需要的空间数目为4*N

显然,构造线段树是一个递归的过程,伪代码如下:

//构造求解区间最小值的线段树 function 构造以v为根的子树   if v所表示的区间内只有一个元素      v区间的最小值就是这个元素, 构造过程结束   end if   把v所属的区间一分为二,用w和x两个节点表示。   标记v的左儿子是w,右儿子是x   分别构造以w和以x为根的子树(递归)   v区间的最小值 <- min(w区间的最小值,x区间的最小值) end function

线段树除了最后一层外,前面每一层的结点都是满的,因此线段树的深度

h =ceil(log(2n -1))=O(log n)。

(2)    区间查询

区间查询指用户输入一个区间,获取该区间的有关信息,如区间中最大值,最小值,第N大的值等。

比如前面一个图中所示的树,如果询问区间是[0,2],或者询问的区间是[3,3],不难直接找到对应的节点回答这一问题。但并不是所有的提问都这么容易回答,比如[0,3],就没有哪一个节点记录了这个区间的最小值。当然,解决方法也不难找到:把[0,2]和[3,3]两个区间(它们在整数意义上是相连的两个区间)的最小值“合并”起来,也就是求这两个最小值的最小值,就能求出[0,3]范围的最小值。同理,对于其他询问的区间,也都可以找到若干个相连的区间,合并后可以得到询问的区间。

区间查询的伪代码如下:

// node 为线段树的结点类型,其中Left 和Right 分别表示区间左右端点 // Lch 和Rch 分别表示指向左右孩子的指针 void Query(node *p, int a, int b) // 当前考察结点为p,查询区间为(a,b] {   if (a <= p->Left && p->Right <= b)   // 如果当前结点的区间包含在查询区间内   {      ...... // 更新结果      return;   }   Push_Down(p); // 等到下面的修改操作再解释这句   int mid = (p->Left + p->Right) / 2; // 计算左右子结点的分隔点   if (a < mid) Query(p->Lch, a, b); // 和左孩子有交集,考察左子结点   if (b > mid) Query(p->Rch, a, b); // 和右孩子有交集,考察右子结点 }

可见,这样的过程一定选出了尽量少的区间,它们相连后正好涵盖了整个[l,r],没有重复也没有遗漏。同时,考虑到线段树上每层的节点最多会被选取2个,一共选取的节点数也是O(log n)的,因此查询的时间复杂度也是O(log n)。

线段树并不适合所有区间查询情况,它的使用条件是“相邻的区间的信息可以被合并成两个区间的并区间的信息”。即问题是可以被分解解决的。

(3)    区间修改

当用户修改一个区间的值时,如果连同其子孙全部修改,则改动的节点数必定会远远超过O(log n)个。因而,如果要想把区间修改操作也控制在O(log n)的时间内,只修改O(log n)个节点的信息就成为必要。

借鉴前一节区间查询用到的思路:区间修改时如果修改了一个节点所表示的区间,也不用去修改它的儿子节点。然而,对于被修改节点的祖先节点,也必须更新它所记录的值,否则查询操作就肯定会出问题(正如修改单个节点的情况一样)。

这些选出的节点的祖先节点直接更新值即可,而选出的节点的子孙却显然不能这么简单地处理:每个节点的值必须能由两个儿子节点的值得到,如这幅图中的例子:

这里,节点[0,1]的值应该是4,但是两个儿子的值又分别是3和5。如果查询[0,0]区间的RMQ,算出来的结果会是3,而正确答案显然是4。

问题显然在于,尽管修改了一个节点以后,不用修改它的儿子节点,但是它的儿子节点的信息事实上已经被改变了。这就需要我们在节点里增设一个域:标记。把对节点的修改情况储存在标记里面,这样,当我们自上而下地访问某节点时,就能把一路上所遇到的所有标记都考虑进去。

但是,在一个节点带上标记时,会给更新这个节点的值带来一些麻烦。继续上面的例子,如果我把位置0的数字从4改成了3,区间[0,0]的值应该变回3,但实际上,由于区间[0,1]有一个“添加了1”的标记,如果直接把值修改为3,则查询区间[0,0]的时候我们会得到3+1=4这个错误结果。但是,把这个3改成2,虽然正确,却并不直观,更不利于推广(参见下面的一个例子)。

为此我们引入延迟标记的一些概念。每个结点新增加一个标记,记录这个结点是否被进行了某种修改操作(这种修改操作会影响其子结点)。还是像上面的一样,对于任意区间的修改,我们先按照查询的方式将其划分成线段树中的结点,然后修改这些结点的信息,并给这些结点标上代表这种修改操作的标记。在修改和查询的时候,如果我们到了一个结点p ,并且决定考虑其子结点,那么我们就要看看结点p 有没有标记,如果有,就要按照标记修改其子结点的信息,并且给子结点都标上相同的标记,同时消掉p 的标记。代码框架为:

// node 为线段树的结点类型,其中Left 和Right 分别表示区间左右端点 // Lch 和Rch 分别表示指向左右孩子的指针 void Change(node *p, int a, int b) // 当前考察结点为p,修改区间为(a,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); // 维护当前结点的信息(因为其子结点的信息可能有更改) }

求区间的和,并可更新,线段树 最简单的线段树  单点更新

#define N 50005int num[N];//N个节点struct cam{int x;//起点int y;//终点int sum;//总数}list[N*4];void build(int k,int x,int y){int mid;list[k].x=x;list[k].y=y;if(list[k].x==list[k].y)//k为叶子节点{list[k].sum=num[x];return;}mid=(x+y)>>1;build(k<<1,x,mid);build(k<<1|1,mid+1,y);list[k].sum=list[k<<1].sum+list[k<<1|1].sum;}int find(int k,int x,int y)//查询区间(x,y)的和{int mid;if(list[k].x==x&&list[k].y==y)return list[k].sum;//如果找到了当前的区间【x,y】则返回mid=(list[k].x+list[k].y)>>1;if(x>mid)//该区间在k的右边的孩子return find(k<<1|1,x,y);else if(y<=mid)//该区间在k的左边的孩return find(k<<1,x,y);//查询的区间很跨k的左右return find(k<<1,x,mid)+find(k<<1|1,mid+1,y);}void update(int k,int x,int y)//将(x,x)的叶子节点的和置为y{int mid;if(list[k].x==x&&list[k].y==x){list[k].sum=y;return;}mid=(list[k].x+list[k].y)>>1;if(x<=mid)//叶子节点在k的左边update(k<<1,x,y);elseupdate(k<<1|1,x,y);//回溯更新(x,x)的所有父节点list[k].sum=list[k<<1].sum+list[k<<1|1].sum;}int main(){int k,t,a,b;int i,n,m;char str[10];scanf("%d",&t);for(k=1;k<=t;k++){scanf("%d",&n);for(int i=1;i<=n;i++){scanf("%d",&num[i]);}build(1,1,n);printf("Case %d:\n",k);while(scanf("%s",str),strcmp(str,"End")){scanf("%d%d",&a,&b);if(strcmp(str,"Query")==0){printf("%d\n",find(1,a,b));}else if(strcmp(str,"Add")==0){num[a]+=b;m=num[a];update(1,a,m);}else if(strcmp(str,"Sub")==0){num[a]-=b;m=num[a];update(1,a,m);}}}return 0;}


原创粉丝点击