线段树模板

线段树是我接触到的第一个高级数据结构,也是我接触过的模板最长的一种代码,但他的实用性真的很强,绝大部分的区间问题都可以用线段树解决点,所以今天我打算简单介绍一下我对线段树的理解

先放一张百度来的线段树图片:

 线段树大概就长这个样子,我们可以清楚的看到,在树的每一层中,所有的区间和恰好就是整个区间,而且同一层中的不同区间不会存在交集。

先来说一下线段树可以进行的操作,可以区间修改,区间查询,单点修改,单点查询,复杂度都是(logn),这时候可能就会问了,我们通过数组进行单点修改和单点查询复杂度是o(1),为什么还要学复杂度更高的呢?这时候又会有同学说了,涉及到区间修改和查询的可以用前缀和和差分啊?但是需要注意的是前缀和和差分主要针对的是静态的,而不是动态的区间值问题. 虽然用数组单点修改和单点查询复杂度比较低,但是区间修改和区间查询复杂度都是o(n),所以总体来说复杂度就是o(n).

下面说一下线段树的几个具体操作:

看这个图有没有一种二分的感觉?其实线段树的基本思想就是二分。

我习惯是用数组实现线段树,一般需要维护4个数组

l[id]:标号为id的区间的左边界

r[id];标号为id的区间的右边界

sum[id]:标号为id的区间的所有数的和(根据题目不同,这个数组具有不同的含义)

lazy[id]:标号为id的区间的子区间需要进行的更新操作

建树代码:

void build(int id,int L,int R)
{
	//建树时要在一开始就初始化 
	l[id]=L,r[id]=R,sum[id]=0,lazy[id]=0;
	if(L==R)
	{
		sum[id]=a[L];
		return ;
	}
	int mid=L+R>>1;
	//递归建立两个子节点 
	build(id<<1,L,mid);
	build(id<<1|1,mid+1,R);
	pushup(id);//用子节点更新父节点 
}

我们在建树时一开始是先递归到叶子节点再对父亲节点进行更新的,在回溯过程中我们需要进行更新操作,更新方式根据题意的不同而不同,以总和为例,更新代码:

//只有区间修改和点的修改才会加pushup操作
//id代表当前区间的编号 
void pushup(int id)
{
	//这里不一定是求和,还可以是其他具有共性的性质,比如最大值最小值公约数等等 
	sum[id]=sum[id<<1]+sum[id<<1|1];
}

接下来是单点更新操作,就是先递归找到这个点,然后进行更新,在回溯过程中对父节点更新一下就好,代码:

void update_point(int x,int id,int val)
{
	if(l[id]==r[id])
    {
        sum[id]=val;
        return ;
    }
	int mid=l[id]+r[id]>>1;
	pushdown(id);//题目中若无区间更新操作,则无需添加 
	if(x<=mid) update_point(x,id<<1,val);
	else update_point(x,id<<1|1,val);
	pushup(id);
}

单点查询操作与单点更新操作极其相似,也是先递归找到这个点,然后返回这个点的值就行了,与单点更新就差一个pushup操作,代码:

void query_point(int x,int id)
{
	if(l[id]==r[id]) return sum[id];
	int mid=l[id]+r[id]>>1;
	pushdown(id);
	//判断查询节点属于哪棵子树中 
	if(x<=mid) query_point(x,id<<1);
	else query_point(x,id<<1|1);
}

接下来就是区间更新操作了,区间更新操作与单点更新操作不同,并不是一定要递归到所需更新区间的每个点进行更新,而是直接对其子区间的sum数组进行更新,并打上懒标记,如果需要用到其子区间的sum,需要先更新再使用,这也就是懒标记的作用.下面是pushdown操作代码:

//涉及到区间修改的题目一般都需要加pushdown操作,也因此需要加lazy数组
void pushdown(int id)
{
	if(lazy[id])
	{
		//用父节点的lazy数组去更新子节点的lazy数组 
		lazy[id<<1]+=lazy[id];
		lazy[id<<1|1]+=lazy[id];
		//子节点的sum要加父节点的lazy标记乘以子节点对应区间的长度 
		sum[id<<1]+=(r[id<<1]-l[id<<1]+1)*lazy[id];
		sum[id<<1|1]+=(r[id<<1|1]-l[id<<1|1]+1)*lazy[id];
		lazy[id]=0;//千万不要忘记清空父节点的lazy数组 
	}
}

区间修改就是先递归找到目标区间的子区间,然后对其sum数组进行操作,并打上懒标记,注意找子区间前一定要进行pushdown操作,防止子区间需更新但还未更新,导致使用错误的sum数组的值.,在回溯的时候要进行pushup操作,对其父节点进行更新,下面是代码:

//区间修改中的L,R代表目标区间的左右边界 
void update_interval(int id,int L,int R,int val)
{
	//当前区间不在目标区间中,直接返回 
	if(l[id]>R||r[id]<L) return ;
	//当前区间是目标区间的子集,直接进行更新,做上lazy标记后就无需递归子节点 
	if(l[id]>=L&&r[id]<=R)
	{
		sum[id]+=val*(r[id]-l[id]+1);
		lazy[id]+=val;
		return ;
	}
	//千万别忘记pushdown操作,有可能之前做过lazy标记但还未更新 
	pushdown(id);
	//递归两个子节点 
	update_interval(id<<1,L,R,val);
	update_interval(id<<1|1,L,R,val);
	pushup(id);
}

区间查询操作比较简单,准确来说就是把目标区间分成若干个已经划分好的区间,这些区间都是无交集的,但这些区间并不一定在树的同一层,我们只需要返回这些区间的sum值即可,代码:

int query_interval(int id,int L,int R)
{
	if(l[id]>R||r[id]<L) return 0;
	if(l[id]>=L&&r[id]<=R)	return sum[id];
	pushdown(id);
	return query_interval(id<<1,L,R)+query_interval(id<<1|1,L,R);
}

上面就是线段树的基本代码了,大家可以按照我一开始放置的百度图片进行理解,线段树模板比较长,有时候也需要灵活转变,所以一定要深刻理解他的本质,在接下来的博客中我会放一些线段树的题目,如果大家有什么好的线段树题目,欢迎在评论区里面分享!

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值