链可以看做是一种特殊的树,当一棵树退化成了链,某些问题就变得容易解决,也有很多数据结构支持这样那样的操作,线段树就是其中一种。从而我们可以想象,能否把一棵树也分成很多条链,从而来解决一些问题呢?这就有了树链剖分。
形象点说,树链剖分就是把一棵树分成多条链(称为“重链”),链与链之间有一些边相连,当操作在链上时,可以用线段树来维护,但在链与链之间的边(称为“轻边”,也叫“轻链”)就需要直接维护。在重链上,维护的时间复杂度为log级别的,而轻链需要一个个维护,如此看来,树链剖分的时间复杂度与轻链的条数有着直接的关系。那么就有一个问题,如何剖分才能够使轻链尽可能的少?
树链剖分是这样解决这个问题的:首先,每个点都必须在重链上(一条重链可以只有一个点),那么就需要把其中的一些重链加入一些点,使得重链的条数减少。对于一棵树,从下往上处理(即先处理深度比较大的)。size(i)记为以结点i为根的子树的结点数,某个点的重儿子就是指该结点儿子中size值最大的一个,其余的儿子称为“轻儿子”(也可能没有儿子)。每个结点加入它重儿子所在的重链。如图:点1的重儿子是点3,点3的重儿子为点6,点4的重儿子为点8或点9(若size值相等,则任选一个作为重儿子,下面的例子是取点8)。
我们按深度处理,除黑色代表未处理的点,不同的颜色为不同的重链:
如样例,就有五条重链,四条轻链(黑色的边)。现在需要证明的是,按照如此剖分的方法,从任意一点到根结点经过的轻链不超过log n条。假设是从点i出发。若i向上走一步,经过了一条轻链,来到了它的父亲k(如图点5走一步到点3),size(k)必然大于size(i)* 2 ,因为点i不是点k的重儿子,点k必然有一个重儿子j,使得size(j)≥size(i),那么自然有size(k)>size(i)* 2 。也就是说,每经过一条轻链,size(i)就要翻一倍,所以最坏情况下只会翻倍log n次,如果超过了log n,那么这棵树的结点就要超过n啦,是不符题意的。
通过上面的结论,显然:从任意一点到根结点经过的重链也不超过log n条。
如此看来,从任意一点到另一点,经过的轻链不会超过2 * log n。时间复杂度就得到了保障,加上重链的维护,那就是O(2 * log n + 2 * log n * log n),这是最坏情况,一般很难达到。
下面再说说具体实现的步骤,是对于点的修改和询问。若是基于边的修改和查询,那么把每条边的权值附在它下面的那个点上(如样例,连接点3和点6的那条边的值就保存在点6上),这样能够保证每条边都有可依附的点。
有一种写法是把所有的点都放进一棵线段树,这样代码是短,但我觉得比较慢。起码比下面的方法慢3倍。
我写的是多棵线段树,用指针实现。剖分的过程用宽搜。
npath:重链条数。
top(i):重链i最顶端结点。
len(i):重链i的长度。
belong(i):结点i属于哪条重链。
idx(i):结点i所在的重链的编号。如样例,绿色重链中,idx(10) = 0, idx(6) = 1, idx(3) = 2, idx(1) = 3。
dep(i):结点i的深度。
size(i):以结点i为根的子树的结点个数。
father (i):结点i的父亲。
还有,对于两点操作,并不需要找它们的最近公共祖先,只需要看它们所在重链的顶端结点的深度,深度低的往上跳,直到两点在同一重链。
具体的实现看qtree模板。