线段树是信息学奥赛中常用到的一种数据结构。它的功能和树状数组类似,但是在一定程度上要比树状数组更强大,但是相对来说也存在着代码实现较麻烦,空间复杂度较高,但是这些缺点并不影响它成为一种强大的实用的数据结构。
线段树主要支持的功能为:查询区间和,查询区间最值,单点修改以及区间修改。
本文将主要讲解线段树是如何实现这些功能的。它的原理主要是凭借二分区间,因为对于一组排序好的数,通过二分法查询,可以在㏒₂(n)次查询中得到,而如果我们顺序查询的话,平均需要n次查询,显然,这样的效率是非常差的。所以线段树的使用是十分有必要的。
而线段树实际上是一棵二叉树,每个节点都代表了一组数中的一段值。因此我们可以通过一个图表来理解(来源:百度):
由图可知对于区间[1,10],根节点就是[1,10],然后将10二分为它的儿子,建立[1,5],[6,10]的两个区间,同理一直进行同样的操作,直到每个节点变成一个点(我们将它成为叶子节点),即像[1,1][2,2][3,3]这样的节点。
通过这个图我们可以清晰的发现,对于每棵线段树都可以分成两个小的线段树,直到叶子节点。那么建立一棵线段树的方式就很清晰明了了。我们建立一个build过程,build(x,y,z),因为对于线段树我们通常使用数组实现,那么结合下图:
1
/
2 3
/ /
4 5 6 7
/ /
8 9 (10)(11)
对于这个图我们很容易发现,如果给一棵二叉树编号,那么一个根节点的左儿子的编号,是该点编号的2倍,右儿子是2倍+1;
因此我们可以用build(x,y,z)里的x代表以x点为根建立一棵线段树(上文提到过一棵线段树能分成很多个小的线段树),区间为[y,z],因此,build(x,y,z)后再build(x*2,y,(y+z)/2)和build(x*2+1, (y+z)/2+1,z)然后直到y=z,那么就代表已经建立到了一个叶子节点,然后将这个点赋值为单点的值,然后向上更新每个点的值即可。
这样就可以建立一个线段树模型了。
而对于线段树的操作主要有,更新(add),查询区间和(query),查询最值(MAX,MIN)。
而对于更新操作(add)主要分为单点更新和区间更新。首先我们先进行单点更新。因此我们需要找到这个点,我们可以顺序寻找这个线段树的每个节点,但是仔细想一想,这样是非常不划算的,因为每一个单点所对应的点,一定是在整个线段树的最下面,如果顺序寻找,那么花费的时间将会是巨大的。但是我们结合线段树的性质来看,就会发现,举个栗子:如果在[1,10]中要修改第二个点的值,那么这个点一定在[1,5]的区间中,而又在[1,3]中,那么这样就变成了,在一段区间中,如果要修改的值小于这个区间的中点,那么就找它的左儿子,反之,则寻找它的右儿子。因此这样只需要㏒₂(n)次查询就可以得到,这无疑是非常划算的,而在修改之后要记住回溯修改他的父亲节点。
而如果是区间修改,我们可以采用同样的方法,寻找包含修改区间的区间,举个栗子对于[1,10]如果我们修改[3,8]的值,我们会发现只有[1,10]完全包含它,而剩下的两个区间[1,5][6,10]都不完全包含这个区间,别虚!我们只需要将这个区间一分为二成[3,5][6,8]两个区间不就好了?因此,我们这样就可以对区间进行修改了。
但是我们会发现一个问题,对于区间修改,如果这个区间被修改了,那么显然它包含的每个小区间都应该被修改。而当修改的区间较大时,需要修改的区间显然是十分多的,显然这样是十分不划算的,因为很少有问题会锲而不舍的将每一个区间都询问到,它只会询问每个区间包含的一两个小区间,显然,这样一对比,是非常不划算的。
因此这个时候我们需要引入lazy思想,简单来说就是拖延症,如果对于一个区间进行修改,这个时候按理说我们要修改每个小区间,但是,这个时候我们的线段树是一个重度拖延症患者,它给这个区间打一个lazy标记,表示这里有任务没有完成,但是它不做,只有当询问到这个标记一下的区间时,它才会向下更新,但是并不是全部更新,对于一个拖延症来说,中心思想就是能拖则拖,我们只需要更新到询问的节点就好了,再在这里打一个lazy标记,然后剩下的就再次拖着。事实证明,就如老师一般不会检查所有的作业一样,大部分修改是没有用的,你拖到问题结束也没有询问,因此你就剩下了大把大把的时间。因此lazy思想对线段树的优化是简单而行之有效的。
而对于查询最值就更加简单了,我们只需在更新每个节点时记录当前区间的最大值就好了,注意!单点修改和区间修改都可以对其造成影响。因此要注意更新区间和的同时更新最值。
而最后的查询就更为简单了,他的方法实际上是和修改一样的,只是把修改的操作变成了查询,同时应当注意的是在查询的时候要注意,如果有lazy标记就先更新,再查询。
字数还有点不够,那我就稍微的水下字数。
拓展:
1.对于一组数,求每个数前的第一个比它大的数,除了用单调栈以外显然还可以用线段树来求区间最值。
2.对于区间更新,我们除了进行加减,还有乘法运算时应该怎么办,很简单,打两个lazy标记,一个表示加减,一个表示乘除,加减操作直接加减对应的lazy标记,而有乘法时应当先对加减的lazy标记进行乘法运算,再乘乘法的lazy标记,然后在更新他的儿子的时候先进行乘法运算,再进行加减,这样就可以完成了。
说了这么多,一棵线段树的基本功能就讲完了。