蒟蒻的学习之——线段树1

线段树用于处理什么问题?

答:基于二叉树数据结构基础,进行如下几种操作

单点修改/区间修改

单点询问/区间询问

能在nlogn的时间复杂度下解决问题

线段树就是将一段区间以不断二分的形式拆分成一个个单点

1. 建树与维护

首先就是线段树的结点怎么存,方法有很多,我喜欢结构体首先就是线段树的结点怎么存,方法有很多,我喜欢结构体

struct node
{
	ll sum;
	ll tag;
}t[maxn<<2];
别忘了空间开到4*n,tag是懒惰标记,特别有用,接下来会讲

接下来就是递归建树,build(k,l,r)表示当前要构建区间[l,r]的线段树,k表示区间[l,r]所对应的标号。建树过程就是不断二分,直到到达叶子结点,就输入当前结点的值。

void build(ll k,ll l,ll r)
{
	if(l==r)
	{
		scanf("%lld",&t[k].sum);
		return;
	}
	ll mid=(l+r)>>1;
	build(k<<1,l,mid);
	build(k<<1|1,mid + 1,r);
	update(k);
}

维护
维护就是update函数,操作的目的是为了维护父子节点之间的逻辑关系。当我们递归建树时,对于每一个节点我们都需要遍历一遍,并且电脑中的递归实际意义是先向底层递归,然后从底层向上回溯,所以开始递归之后必然是先去整合子节点的信息,再向它们的祖先回溯整合之后的信息。

void update(ll k)
{
	t[k].sum = t[k<<1].sum + t[k<<1|1].sum;
}
k<<1和k<<1|1 是位运算,当然不是用来装逼 ,肯定能提速

2. 区间修改

为什么不讨论单点修改?因为其实很显然,单点修改就是区间修改的一个子问题而已,即区间长度为1时进行的区间修改操作。

那么对于区间操作,我们考虑引入一个名叫“lazy tag”(懒惰标记)的东西——称其lazy的原因是因为原本区间修改需要通过先改变叶子节点的值,然后不断地向上递归修改祖先节点直至到达根节点,时间复杂度最高可以到达O(nlogn)的级别。但当我们引入了懒惰标记之后,区间更新的期望复杂度就降到了O(logn)的级别且甚至会更低。

比如上面的例子,你要对2-5号点加1,朴素做法时间复杂度O(n),而用线段树区间修改是怎样的呢?

从线段树的顶端开始,如果当前枚举到的区间被要加v的区间完全包含,就在这个区间进行加法操作,把这个区间加上要加的数v乘上这段区间的元素(点)个数,再记录一下这段区间被加过v,就不再往下枚举了。
如果不被完全包含,就接着二分,枚举当前这一段的前半段和后半段

还是拿1~8那个图举栗子。

我们从最上面开始,发现当前枚举到的区间是1-8,而要修改的区间是2-5,并没有完全包含,于是我们开始枚举它的前半段和后半段(1-4和5-8)。

再枚举1-4和5-8,发现仍没有被2-5完全包含,所以继续二分,枚举1-2,3-4,5-6,7-8.
区间修改示意

注意!这时我们发现3-4被2-5完全包含了!!

将3~4这段区间加上元素个数(右端点-左端点+1)× 要加的数v,不再二分它。

注意!我们发现7-8已经完全不被2-5包含了!

对于完全离经叛道的区间,我们要及时return,不再二分它,不然整个算法的时间复杂度将退化成nlogn。

然后发现其它区间仍然不满足,接着二分其它的区间(1-2,5-6)
区间修改2
现在我们的区间经过层层二分已经变成单点了,我们把目前被包含的单点加上v;

void modify(ll k,ll l,ll r,ll x,ll y,ll v)
{
	if(x<=l&&r<=y)
	{
		t[k].sum+=(r-l+1)*v;
		t[k].tag+=v;
		return;
	}
	if(r<x||y<l) return;
	pushdown(k,l,r);
	ll mid=(l+r)>>1;
	modify(k<<1,l,mid,x,y,v);
	modify(k<<1|1,mid+1,r,x,y,v);
	update(k);
}

懒惰标记的作用是记录每次、每个节点要更新的值,而线段树的优点在于传递式记录:

如果整个区间都被操作,记录在公共祖先节点上;只修改了一部分,那么就记录在这部分的公共祖先上;如果只修改了自己的话,那就只改变自己。

如果我们采用上述的优化方式的话,我们就需要在每次区间的查询修改时pushdown一次,以免重复或者冲突或者爆炸

那么对于pushdown而言,其实就是纯粹的update的逆向思维(但不是逆向操作): 因为修改信息存在父节点上,所以要由父节点向下传导lazy tag

怎么传导pushdown呢?这里很有意思,开始回溯时执行update,因为是向上传导信息;那我们如果要让它向下更新,调整顺序,即在向下递归的时候pushdown就可以了

void pushdown(ll k,ll l,ll r)
{
	if(t[k].tag==0) return;
	ll mid=(l+r)>>1;
	t[k<<1].sum += t[k].tag * (mid - l + 1);
	t[k<<1|1].sum += t[k].tag * (r - (mid + 1) + 1);
	t[k<<1].tag += t[k].tag;
	t[k<<1|1].tag += t[k].tag;
	t[k].tag = 0;
}

3. 区间查询

区间查询的思路跟区间修改大致差不多

ll query(ll k,ll l,ll r,ll x,ll y)
{
	ll ans=0;
	if(x<=l && r<=y) return t[k].sum;
	pushdown(k,l,r);
	ll mid = (l + r) >> 1;
	if(x<=mid) ans+=query(k<<1,l,mid,x,y);
	if(mid<y) ans+=query(k<<1|1,mid + 1,r,x,y);
	return ans;
}

蒟蒻还学的云里雾里,哈哈哈,继续努力!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值