线段树学习笔记
线段树是一个完全二叉树,(除了最下面其他地方都有两个儿子)第一个节点表示的信息为整个数组中的信息(例如最大元素,或全部元素的和),左儿子为【1,m】个元素的信息,右儿子为【m+1,n】个元素的信息,m设为(1+n)/2; 也就是说我相当于把整个数组打成两段,用递归的方式求解。例如我要求整个数组中最大的那个数,那我可以先求出【1,m】中的最大数,然后再求出【m+1,n】中的最大数,取其较大,同理递归。直到区间长度为1,及可以直接读出最大值就返回。最下面的叶子为原来的那个数组。
详细说明上述简单例子
题目:给一个数组a[n],给定一个操作query(i,j),求第i到第j个元素之和。
第一步:建树
首先用a[n]数组储存这些数,然后第二步写建树函数void build(int l,int r,int rt);先给出代码
先看下面一个函数Build,sum数组就是一个线段树(二叉树,然后第i个节点的左子树为2*i,右子树为2*i+1,sum[0]为树的根)
按照上面的说法左右两边递归,l,r表示的是这个节点区间。rt表示这个节点的下标(也可以用结构体封装起来)如果l==r就是区间长度为1,到达叶子节点,就填上去。return很重要。填完了表示这次的建树就结束了,所以一定要return(不知道在这里死了多少次了= =)如果没有就继续左边build,右边build。两边左节点和右节点填完了以后就要填父节点让这个树往上走。最后加一个pushup函数,这个函数就是求和,让父节点等与两个子节点的和,然后一步一步往上加。这里稍微总结下,一开始提到了父节点表示的是两个子节点的和,然后的话我们将整个区间不断的对半分割递归。可以理解为最开始的是整个数组区间储存再根节点中,然后左边存一段,右边存一段。将整个数组的信息分别存到他的两个子节点中,直到最后那个区间就表示的一个数组的元素。如果单单看叶子的话就是原来的数组。然后在实际建树的过程中,我们运用了递归栈来逆推这个过程,我已经知道了叶子节点的信息了,用pushup函数求父节点的信息。
ps:这里用到了位运算,rt<<1表示二进制右移1位,相当于成2,”|1“表示与1求或运算,因为之前已经乘以2了所以末尾必然为0,与1求或运算就相当于加1
第二步:查询操作
函数Query(int l,int r,int L,int R,int rt)先看代码
L,R表示的是查询的区间,l,r表示的是当前的线段区间,rt是该区间表示的节点的下表(同上),查询操作的基本思路是,将查询的区间分割到线段树中的每一个尽可能大的区间。下面举个例子
如图,我要查询2-12的和,显然2-12不是线段树某个完整的区间(如果是的话就像代码2-4行可以直接读出长度,这个后面会再谈到的),首先要查询的区间(2-12)是肯定在根节点的区间(1-13)中的,但是有一部分再左节点(1-7)中,还有一部分在右节点(8-13)中,query函数是求L-R,在区间l-r中的和的,他的返回值就是这个和,此时可以递归累加,将求和总区间(根节点)分成左边和右边(l-m和m+1,r,此处m为(l+r)/2)(代码10-11行),求完后累加就return。递归停止条件就是上面提到的,当要求的区间包括了当前的区间时返回,因为此时该区间已经全部需要加上去了,不需要分割(代码2-4行)
修改某个端点的值
方法void update(int L, int c, int l, int r, int rt); 用来表示将原数组的第L个数增加c,后面三个参数表示的是线段树的信息,即区间l-r, 第rt 个节点。给出代码
先递归的找到线段树中需要修改的节点,代码8-9行中给出了,一个类似二分查找的方法找第L个数在线段树中的位置。如果找到了,即到达根节点,那么就修改(代码2-3行)。A题中的那个还有减少和增加,减少可以看作增加负数,通用一个update方法。叶子节点修改了以后,他向上的每个父节点和祖先节点都要修改(相当于重新建树,但是只用建一部分,也就是修改过那个部分),只需要在递归修改完了,返回父节点的操作后面调用pushup,上文谈到的更新操作。
B题中的修改为直接更改值,也是一样的方法。将到叶子节点的操作重写就行了。然后B题需要的信息是最大值,不是A题中所需要的和。建树的时候更新信息的那个函数pushup中应该改为求两个子节点中最大的那个。而查询操作不是求和所以不需要累加,而是判断左边和右边的哪个更大,返回那个更大的就好了。