题目
题目大意
维护一个无向图的割边条数,支持加边和删边。
正解
(PS:这是我很久之前在OJ上打出来的题解,现在直接copy过来)
题解只有一句话,估计没多少人可以看得懂。感觉出题人偷懒不想写题解……
刚了一个晚上终于理解了题解的做法……
由于本人还没有AC(时间比较匆忙),所以只是在这里梳理一下思路,顺便造福一下人类。
首先都知道线段树分治是个什么意思吧?
线段树分治是一种有效地利用撤销操作替代删除操作的套路。在这题中,所有的加边删边操作变成了加边和撤销操作。可以把操作看成一个栈,加边的时候就是入栈,撤销的时候只会从后往前撤销,就是弹栈。
先离线一波,处理出每一条边存活的时间区间,然后将它挂在线段树上。学过线段树的人都知道它会覆盖线段树上的O(lg n)个节点。
接下来遍历整棵线段树。在经过一个节点的时候,就将挂在上面的所有边加进来。因为这些边覆盖了这个节点,表示这些边出现的时间区间完全覆盖这个节点所代表的区间,也就意味着整个区间内都会有这些边。一直遍历下去,到了叶子结点就可以统计答案了。回溯的时候,倒着将挂在节点上的边给撤销掉。
在这题当中可以用LCT来实现,不过是O(n lg^2 n)的,会TLE。
上面的实际上是49分的做法……现在来讲讲题解的100分做法。
在某条边的时间区间加入线段树的时候,它会经过一些节点。学过线段树的人都知道这些节点的个数是O(lg n)个的,并且包含它覆盖的节点(实际上经过的点就是覆盖的点到根路径的并)。我们不仅将边挂在时间区间覆盖的节点上,再另外用个数组(或链表)将它挂在经过的点。
挂在经过的点上意味着时间区间和节点表示的区间有交集,也就是说这个边会对子树中部分叶子结点的答案会有影响。
在遍历线段树的时候记下当前的割边个数。我们存下每一层的图(其实是森林)。每当到了一个节点,先把上一层的图给复制过来。将挂在节点上的所有边的端点搞出来,成为一个点集。我们称之为有用的点集。对于点集外面的点,后面就不需要理它了,就可以把它给缩起来。怎么缩点?实际上就是找出有用的点集,然后建虚树。
图中的每条边都要记下这条边上有多少条割边。在建虚树缩边的时候,就要将路上的边记录的割边数加起来。
建虚树的过程可以用O(原图点数)的方法,原图就是上一层的图。具体来说,就是dfs一遍,对于每棵子树,维护子树当中是否有有用点,如果有就记有用点的编号。回溯的时候,如果两个或以上个子树中有有用点,那么它也要被加进虚树内。否则它就不需要加。
在进行了上面的操作之后,整个图只剩下有用的点。接下来就将覆盖这个节点的边一鼓作气地全部加进来。加了之后跑一遍Tarjan,将环缩成点(其实也不用打Tarjan,具体来说,如果出现了返祖边,就直接将这里到祖先缩起来就可以了,毕竟这是个无向图)。
当然,在加边、缩点的过程中,记得维护一下答案(建议答案不要在最后计算,因为在建虚树的时候,有些无用的点可能会是叶子结点,它连着的那条边是永远都不会被缩掉的。如果在最后统计答案,就会将它给忽略;不过实际上也可以记一下无用的点并且还是叶子结点的贡献,在最后统计时加上。)
时间复杂度自己随便想一想就知道了,此处不再赘述。
这个东西实现起来肯定是很麻烦的。先要预处理出所有边的存活时间;其次是维护一棵线段树,每个节点维护有哪些边覆盖这里,哪些边经过这里;然后在遍历线段树的过程中,每一层都要存一个图(实际上这个图是边双的图),图的每条边要记录边所代表的割边数量,记录有哪些有用点,维护一个并查集表示有用点在哪个边双,还有个答案;还要加边和缩点操作,打Tarjan或者直接用并查集来缩。
所以说,时间复杂度要乘上一个巨大的常数……
没有代码,根本没有时间去打……
%%%LYL,在耗费了我两小时的假期时间以及话费之后,刚了几星期将这题AC了……
总结
线段树分治是一个常见的套路,作用就是将删除操作变成撤销操作,操作序列变成进栈出栈的形式。
进行覆盖线段树上节点的操作时,经过的点的个数是\(\log\)级别的。
所以记录经过某个点的操作,然后进行各种优化操作,这样子记录的总次数是\(n\log n\)的。