【ALgarth】笔记补完计划 线段树初级内容

前言

前言当然是感谢当时学习线段树时听过的课,包括刘汝佳的三神套,雨巨的牛客网课,学长的笔记和博客呜呜呜

线段树的引入

线段树的引入问题是动态的区间和问题,在刘汝佳紫皮算法起步的时候就有讨论过,这部分内容是以一个例题引入的,题目大意如下

现给定一个区间0-n,同时给定m个长度不同的小区间,小区间包含在大区间的内部,每一个小区间放置在大区间上会使得大区间当前点所在的值增加1,请通过代码求算0-n上最大的区间和,同时输出区间和以及两个下标

这个问题解法很多,问题在于如何简化时间复杂度,假设这个nm都给定了,有一堆区间了,假设如下图
在这里插入图片描述建立一个一位数组v,保存当前位置覆盖的小区间数目。这时问题就转化为两部分了,前期m次操作的预处理和后期的求最大和。

预处理

方法一 枚举

枚举没什么好说的,直接嵌套两层for循环,每次从两个下标直接使用for循环,挨个++就完了,但是问题是题目可能会给一个很大的m,或者说假设这个n和m给定最大到1e5吧,那要是题目给你个痛快的,每次这个m都是1e5,你每次都整个数组全部++,肯定最后TLE。

方法二 前缀和,差分

前缀和:通俗理解,就是前缀取和,通过前缀的方法保存,比如数组t{1,2,4,3,5},前缀和为前面项相加的和,即sum{1,3,7,10,15}.(前缀和一般有第0项,数组t习惯从1开始)

差分:差分刚好是前缀和的逆向应用,他表示的是当前这个数字和前一项的差,假设一个t{1,2,4,3,5},差分minus{0,1,1,2,-1,2}(前面的0是第0项)

前缀和和差分互为逆运算,对一个数组同时使用前缀和和差分结果就是数组本身,这个处理数组的问题完全就可以靠差分解决,先求出差分,每次给出一个小区间之后完全可以靠小区间的两个下标,在差分的对应位置+1和-1,最后使用前缀和一次就可以得到处理完的数据了,时间复杂度直接到O(n)。

后续运算

后续运算就显得比较简单,直接用不着线段树,也用前缀和解决就行了,再求一遍前缀和,之后嵌套循环,求一个最大的区间就行了。

为什么要有线段树

那看到这儿想必就要问了,线段树有个锤子用啊?线段树做的是上述问题的动态分析,比如上述的题目,如果不是说m次操作之后求一个最大区间,而是在每一次操作之后求最大区间,求m次,仍然选用上述操作就会直接TLE,因此,有必要使用线段树解决动态问题。

线段树的理论基础

线段树是树,没学过的树的建议先去看看离散数学,线段树是一个完全二叉树(不同书上叫法不同),叶节点的差值最大为1,这种树可以使用数组模拟,左子和右子直接是父节点的2倍和2倍+1,比较方便。

线段树我不是很想画了,那感谢邰学长的PPT配图在这里插入图片描述
图上就是一颗线段树的表示,这颗线段树是以上面array进行构造的,这棵树目的是求线段和,因此根节点是数组0-5的和,左右两边是左右两半的和,直到无法下分的时候就是当前array数组里面的所在值,这颗树构造出来值为{36,9,27,4,5,16,11,1,3,0,0,7,9,0,0}

线段树的代码实现

void buildtree(int p,int l,int r) {
	if(l==r) {
		v[p]=weight[l];
		return;
	}
	int mid=(l+r)>>1;
	buildtree(2*p,l,mid);
	buildtree(2*p+1,mid+1,r);
	v[p]=v[2*p]+v[2*p+1];
}

p传入的是根节点,方便对树的构造,l和r分别代表整棵树的左界和右界,中间使用递归构造,构造到叶子的时候return

线段树的基本应用

线段树的基本应用分为以下几种:

线段树的单点修改

线段树的单点修改是对原先的数组的一个数值进行的修改,这种修改的方法很简单,就是通过查询,找到要进行修改的位置,修改之后,通过v[p]=v[2 * p]+v[2 * p+1],进行更新中间节点的值,

void changepoint(int p,int l,int r,int x) {
	if(l==x) {
		v[p]=num;
		return;
	}
	if(mid>=x) return changepoint(2*p,l,mid,x);
	else return changepoint(2*p+1,mid+1,r,x);
}

x是传入的修改位置,通过二分判断找到修改的位置,之后将这个位置的值改为num。

线段树的给定区间求和

int sumtree(int p,int l,int r,int rel,int rer) {
	if(l>=rel&&r<=rer) {
		return v[p];
	}
	int mid=(l+r)/2;
	if(mid>=rer) return sumtree(2*p,l,mid,rel,rer);
	if(mid<rel) return sumtree(2*p+1,mid+1,r,rel,rer);
	return sumtree(2*p,l,mid,rel,rer)+sumtree(2*p+1,mid+1,r,rel,rer);
}

给定的区间设为rel-rer,求和的时候,不一定完全找到叶子返回,只要现在的区间是要求区间的子集返回就行

线段树的区间改变

void changetree(int p,int l,int r,int rel,int rer) {
	if(l==r) {
		if(l>=rel&&l<=rer) v[p]+=k;
		return;
	}
	int mid=(l+r)/2;
	changetree(2*p,l,mid,rel,rer);
	changetree(2*p+1,mid+1,r,rel,rer);
	v[p]=v[2*p]+v[2*p+1];
}

这个是一个基本的改变区间的方法,方法很傻,就是整个区间+k,我用的是直接找到叶子+k的方法,如果遇到多次处理动态问题的话很容易TLE,还有一个比较方便处理的方法,懒标记,LAZY数组,这个方法下次在写。

题目集推荐

初学的内容差不多也就这些了,剩下的完全就是做题了,线段树的内容还有一部分文章没讲,包括懒标记,线段树处理其他问题比如最大值之类的,延伸的内容我们就下一篇文章再见。

推荐题目集当然是推荐洛谷的了,附一个洛谷的线段树题库链接

https://www.luogu.com.cn/problem/list?keyword=%E7%BA%BF%E6%AE%B5%E6%A0%91
洛谷的线段树题目集

呐呐呐,这次就先到这里呢

//我是ALgarth,一位c–爆零大师,本科学的JVAV,摸鱼的艺术。永远喜欢Bjarne Stroustrup和他的光头

//图片来自于自己,必不会侵权,有个图是邰学长的呢,侵删

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值