线段树

线段树简介:

        线段树用于对数组的一段区间进行大量的查询或者更新操作。例如给定一个数组,有两种操作,第一种是让某个区间内的数都加上某个值,第二种是查询某个区间内的数的总和。第一种操作我们可以用遍历数组的方法。第二种操作我们可以用一个二维数组来保存区间i到j的和。但是这两种方法组合到一起会非常耗时,最关键的是,而且改变了数组中的数后,对二维数组的维护也很麻烦。这时线段树就可以很好地解决这个问题。

线段树实现方法:

        线段树是一种树形结构,并且是一个完全二叉树。其根节点用来保存整个数组的某个信息,例如总和、最大值最小值等。然后再将数组分为左右两半,由根节点的左右子节点分别保存这两个子区间的信息,然后一直递归下去。不难发现,线段树的叶子结点保存的是长度为1的区间的信息,即叶子结点保存的就是数组中的数。例如一个长度为10的数组,建立其线段树为:

        其中绿色的结点就是叶子结点,即数组中的数,蓝色是非叶子结点,保存一段区间的信息。父节点的信息是由子节点的信息整合而来的。例如结点保存的是区间内数的和,那么父节点的值就是两个子节点的值之和。所以这里我们发现线段树的一个前提条件:相邻的区间的信息可以被合并成两个区间的并区间的信息。例如总和、最大/小值等。

数据结构

  1. num数组,用来保存线段树中节点的值。假设父节点的下标是i,则左孩子下标是2*i+1,右孩子下标是2*i+2。
  2. tag数组,用来表示延迟标记(后面解释)
  3. arr数组,用来保存原数组中元素的值。

线段树的建立

        线段树的建立可以类比二叉树的建立,先从根节点开始递归,将区间层层分割,直到将区间分成长度为1的叶子结点,当我们得到了叶子结点的信息时,可以看作是得到了最小子区间的信息。然后向上回溯,将两个子区间的信息整合并保存到他们的父节点中。代码如下:

//root表示当前递归到的结点下标
//l和r表示该位置的结点所表示的区间起点和终点
void build(int root,int l,int r)
{
    //若区间长度为1,则将其保存为叶子结点,其值就是原数组中对应位置的数。
	if(l==r)
		ans[l]=arr[l];
    //若不是叶子结点则先递归建立其左右子结点
	else
	{
		int mid=(l+r)>>1;
		build(root*2+1,l,mid);
		build(root*2+2,mid+1,r);
        //回溯时将两个子节点的信息整合起来。
		ans[root]=ans[root*2+1]+ans[root*2+2];
	}
}

线段树的查询:

        线段树的查询是怎么实现的呢?因为在线段树中,相邻区间的信息可以被合并成两个区间的并区间的信息,那么我们就将要查询的区间分割成多个区间的并区间。在线段树中递归地查找这些小的区间的信息,再整合到一起。可以类比二叉搜索树查询某个数的过程,先向下递归,若遇到的结点所表示的区间是要查询的区间的子区间,那么就将该结点的值返回。直到我们搜集到所有子区间的信息,最后整合起来。代码如下:

//q_r、q_l表示要查询的区间的起点和终点
//root表示当前遍历到的结点的下标
//nl、nr表示当前遍历到的结点所表示的区间的起点和终点
int query(int q_r,int q_l,int nl,int nr,int root)
{
	//若当前遍历到的区间是要查询的区间的子集,则直接返回此节点的值。 
	if(q_r<=nl && q_l>=nr)
		return ans[root];
	//递归查询其左右子区间,最后将左右子区间的信息整合并返回 
	int res=0;
	int mid=(l+r)>>1;
	if(q_r<=mid)
		res+=query(q_r,q_l,nl,mid,root*2+1);
	if(q_l>mid)
		res+=query(q_r,q_l,mid+1,nr,root*2+2);
	return res;
}

区间的更新

        区间更新的是线段树的核心。更新区间即更新最底层的叶子结点,而上层的结点与叶子结点的值息息相关,牵一发而动全身。如果我们还是按建立线段树的方法,先向下递归,递归到叶子节点时更新值,再回溯上来整合信息,这种更新会很慢,而且某些结点的更新,可能对于我们暂时还用不上。

        所以线段树中给每个结点都引进了一个延迟标记tag。当要对区间进行更新时,我们先对其相关的父节点进行更新,然后将父节点的延迟标记设为区间的改变量,然后进行一次延迟标记向下传递操作,即根据父节点的延迟标记来更新两个子节点的值,再将子节点的延迟标记设为改变量,最后清空父节点的延迟标记。

        当我们遍历到一个延迟标记不为0的结点时,若要递归遍历其子节点,那么就在递归前进行一次延迟标记向下传递操作,更新其子节点的值和延迟标记,并清空父节点的延迟标记。这样,我们就可以加快区间更新的速度。向下传递延迟标记的代码如下:

//向下传递延迟标记函数
//root表示当前要向下传递标记的父节点
//l、r表示父节点所代表的区间的起点和终点 
void pushdown(int root,int l,int r)
{
	int mid=(l+r)>>1;
	//更新左右子节点的tag 
	tag[root*2+1]+=tag[root];
	tag[root*2+2]+=tag[root];
	//更新左右子节点的值 
	ans[root*2+1]+=tag[root]*(mid-l+1);
	ans[root*2+2]+=tag[root]*(r-mid);
	//清空父节点的延迟标记 
	tag[root]=0;
}

        由此我们就可以写出区间更新的代码:

//u_l、u_r表示要更新的区间的起点和终点
//root表示当前遍历到的结点的下标
//nl、nr表示当前遍历到的结点所代表的区间的起点和终点
//val表示区间内所有值的改变量 
void update(int u_l,int u_r,int nl,int nr,int root,int val)
{
	//如果当前遍历到的区间包含于要更新的区间,则直接对当前结点进行更新,并做好延迟标记 
	if(u_l<=nl && n_r>=nr)
	{
		//因为区间内的值都要加上val,则该节点的值要加上(r-l+1)个val 
		ans[root]+=val*(nr-nl+1);
		tag[root]+=val;
		return;
	}
	//对两个子节点更新之前先进行一次延迟标记向下传递操作,清空其延迟标记 
	pushdown(root,nl,nr);
	int mid=(nl+nr)>>1;
	if(nl<=mid)
		update(u_l,u_r,nl,mid,root*2+1,val);
	if(nr>mid)
		update(u_l,u_r,mid+1,nr,root*2+2,val);
	//更新完子节点之后更新父节点 
	ans[root]=ans[root*2+1]+ans[root*2+2];
}

练习题:

洛谷P3372 【模板】线段树

HDU1166_敌兵布阵

HDU1754_I Hate It

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值