线段树:记录区间统计信息。原理是将将[1,n]分解成若干特定的子区间(数量不超过4*n),然后,将每个区间[L,R]都分解为少量特定的子区间,通过对这些少量子区间的修改或者统计,来实现快速对[L,R]的修改或者统计。可以记录的统计值必须符合区间加法。
符合区间加法的例子:数字之和,最大公因数,最大/小值。
不符合区间加法的例子:
众数——只知道左右区间的众数,没法求总区间的众数
01序列的最长连续零——只知道左右区间的最长连续零,没法知道总的最长连续零
基本的数据结构:Node: left, right(左右子树); startIndex, endIndex(统计区间); value(区间统计值)
Interface:
- buildTree: 递归和非递归。 根节点记录整个数组的统计信息,叶子节点的startIndex = endIndex。假设父节点的区间为[start, end],最简单的方式是将左右子树的区间分别设为[start, (start+end)/2], [(start+end)/2+1, end]。若用递归方式实现,结束条件为到达叶子节点(start == end), 否则分别构造左子树和右子树,并用左右子树的value来设置父节点的value.
- query: 给区间[i, j], 得到对应的统计值。用递归方式实现,
结束条件:若当前root.start, root.end刚好等于[i, j],则返回对应的统计值;
否则[i, j]有可能在左子树中或右子树中或左右子树都有。
若root.left.endIndex >= i, left = query(root.left, i, Math.min(root.left.endIndex, j)); //make sure use the min of root.left.endIndex and i
若root.right.startIndex <= j, right = query(root.right, Math.max(root.right.startIndex, i), j);
return left + right; - update: 更新a[i]时,所有区间中包含i的统计信息都要做相应调整。参数root, i, newValue. 以递归方式实现,
结束条件:当前节点为叶子节点(startIndex = endIndex = i), 则更新root.value即可
否则看i 在左子树(i <= root.left.endIndex)还是右子树(i >= root.right.startIndex)中,更新对应子树的value(update(root.left/right, i, newValue));最后别忘了回溯,用更新后的左右子树的值更新父节点的值(root.value = root.left.value + root.right.value).
使用上述实现,每次更新一个值都需要遍历一边树。更机智的做法是不需要确实更新值,而是在节点上记录一个pendingOperation(标记),所有记录了pendingOperation的节点的子树上节点的值都变成无效的,在查询时需要根据标记更新本节点的统计信息。比如,如果本节点维护的是区间和,而本节点包含5个数,那么,打上+1的标记之后,要给本节点维护的和+5。这是向下延迟修改,但是向上显示的信息是修改以后的信息,所以查询的时候可以得到正确的结果。有的标记之间会相互影响,所以比较简单的做法是,每递归到一个区间,首先下推标记(若本节点有标记,就下推标记),然后再打上新的标记,这样仍然每个区间操作的复杂度是O(log2(n))。 标记有相对标记和绝对标记,相对标记是将区间的所有数+a之类的操作,标记之间可以共存,跟打标记的顺序无关(跟顺序无关才是重点),所以可以在区间修改的时候不下推标记,留到查询的时候再下推。
注意:如果区间修改时不下推标记,那么PushUp函数中,必须考虑本节点的标记。
而如果所有操作都下推标记,那么PushUp函数可以不考虑本节点的标记,因为本节点的标记一定已经被下推了(也就是对本节点无效了)
绝对标记是将区间的所有数变成a之类的操作,打标记的顺序直接影响结果,所以这种标记在区间修改的时候必须下推旧标记,不然会出错。