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

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

数据结构之线段树 单点更新_第1张图片

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

//构造求解区间最小值的线段树
 
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)个节点的信息就成为必要。

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

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

数据结构之线段树 单点更新_第2张图片

这里,节点[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 50005
int 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);
	else
		update(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;
}


你可能感兴趣的:(数据结构之线段树 单点更新)