线段树(一)

我们先来看两个问题:

1.给出n个数,n<=100,和m个询问,每次询问区间[l,r]的和,并输出。

确实,这个只需要用前缀和即可,复杂度O(1)

2.给出n个数,n<=1000000,和m个操作,每个操作可能有两种:1、在某个位置加上一个数;2、询问区间[l,r]的和,并输出。

如果枚举就会超时,倘若题目是修改区间【a,b】的值,则此时枚举就更不用说了,所以此时就需要用到线段树了。

先来看一下线段树的概念

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

线段树是建立在线断的基础上,每个结点都代表了一条线段[a,b]。

下图就是一棵长度范围为[1,5][1,10]的线段树。

而且,长度范围为[1,L] 的一棵线段树的深度为log (L) + 1,存储一棵线段树的空间复杂度为O(L)。 

我们还可以知道线段树的两个重要性质:

1、每个节点的左孩子区间范围为[l,mid],右孩子为[mid+1,r]

2、对于结点k,左孩子结点为2*k,右孩子为2*k+1,这符合完全二叉树的性质

线段树都有以下几个操作:

1.创建线段树

2.区间查询

3.区间更新

4.单节点更新

因为线段树是一颗完全二叉树,所以我们可以用数组来建立线段树,但数组的大小需要开满二叉数的大小,我们可以知道第k层的节点数目为2的k次方,所以我们根据树深为log以2为底的n的对数,n为节点个数,由这两个可以知道数组大小应开4n。

1.线段树的建立

const maxn=50005;

int a[maxn];

int f[4*maxn];

void build(int L,int R,int id)
{
    if(R==L)
        f[id]=a[L]; //表示到了叶子节点
    else{
        int mid=(L+R)/2; 
        build(L,mid,id*2); //创建左子树
        build(mid+1,R,id*2+1);  //创建右子树
        f[id]=f[id*2]+f[id*2+1]; //更新每个根节点的值为其左右子树的和
    }
}

2.区间查询

(求某个区间的和)

int query(int id,int R,int L,int l,int r)
{
	if(L>=l&&R<=r)
		return f[id]; //已经达到了最小的区间
	else{
		int mid=(R+L)/2;
		if(r<=mid)
			return query(id*2,mid,L,l,r);  //目标区间全部位于左子树
		else if(l>mid)
			return query(id*2+1,R,mid+1,l,r); //目标区间全部位于右子树
		else                              //目标区间在左右子树都有
			return query(id*2,mid,L,l,r)+query(id*2+1,R,mid+1,l,r); 
	}
} 

3.单点查询

(修改数组内的一个值,例如给某个值加上某个数)

若更新叶子节点,则相应的父节点也改变,所以需要向上回溯

/*
id:当前线段树根节点的下标 
b:叶子节点要加上的值 
L、R:[L,R]当前节点所表示的区间
t:所要改变的节点的下标 
f[]:f数组存放树 
*/ 
void updateone(int id,int L,int R,int b,int t)
{
	if(L==R) 
		f[id]+=b;  //找到叶子节点
	else{
		int mid=(L+R)/2;
		if(t<=mid)   
			updateone(id*2,L,mid,b,t); //从左子树找
		else
			updateone(id*2+1,mid+1,R,b,t);//从右子树找
		f[id]=f[id*2]+f[id*2+1];  //更新相应根节点的值
	}
} 

4.区间更新

区间更新是指更新某个区间内的叶子节点的值,因为涉及到的叶子节点不止一个,而叶子节点会影响其相应的非叶父节点,那么回溯需要更新的非叶子节点也会有很多,如果一次性更新完,操作的时间复杂度肯定不是O(lgn),为此引入了线段树中的延迟标记概念。

延迟标记就是给每个做一个标记,先不往下遍历树修改节点,而是停在那个节点处等到需要对所修改的区间再进行操作时,再向下修改节点,这样修改节点和查询区间合在一起操作,就会降低复杂度。

这里以poj3468为例,具体代码见 https://blog.csdn.net/Krismile_/article/details/84305979

可以跟着代码走一遍,能更好的理解lazy标记。

下面是一些可供参考的练习题:

hdu1166 简单的单点查询、区间求和

poj2352 单点修改、区间查询

poj 3468 区间查询、区间修改

hdu 1457 求区间最大数、单点修改 (https://blog.csdn.net/Krismile_/article/details/84670894

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值