如果公式炸了就请去洛谷博客看这篇题解。
来一篇正经的ETT题解。
窝看到仅有的一篇暴力的题解就生气……这个暴力很脆弱,随便卡就可以卡掉。
前置芝士:ETT(如果你不会,请自行百度,作者会日后写一个学习笔记的,到时候链接附在这里)
基本思想
首先考虑我们用什么来维护一个图,把一个图变成一棵树。
那自然可以想到维护生成树。由于可能不太连通,所以我们维护一个生成树的森林。
加边操作不太难,直接在 ETT 里面 link 一下即可。询问也不太难,在 ETT 里面写一个 findroot 函数即可。
但是遇到删边操作的时候,直接处理是不可行的。因为如果在生成树中删掉一条边,我们需要枚举不在生成树中的边看一下是否可以使得删边之后不连通,那时间复杂度就挂了。
如下图,如果删去边 \((2,7)\),则你需要考虑是否存在 \((5,7)\) 这样的边存在使得图仍旧连通。
那么我们怎么办呢?
考虑建立一个类似分层图的模型。每条边的层不会增加,只会减小。
我们计最高层为 \(\log n\),最底层为 \(1\)。每条新加的边都在 \(\log n\) 层。
令 \(G_i\) 为层数 \(\le i\) 的所有的边和原图中所有点构成的图。显然,\(G_{\log n}\) 就是整个的图。
由层数单调递减可知
\[G_1 \in G_2 \in G_3 \in \cdots \in G_{\log n} \]接下来的两个性质保证了算法的复杂度为 \(O(\log ^2 n)\)。
【性质1】 \(G_i\) 中每个连通块至多有 \(2^i\) 条边。这个应该不难脑补吧……试着想一想 \(G_1\) 和 \(G_{\log n}\)。
【性质2】我们令 \(F_i\) 为 \(G_i\) 的生成树(不一定最小),我们可以使用 ETT 来维护 \(F_i\)。(显然,\(F_{\log n}\) 为用于询问的 ETT)。此时,我们有性质:
\[F_1 \subseteq F_2 \subseteq F_3 \subseteq \cdots \subseteq F_{\log n} \]所以
\[F_i = F_{\log n} \cap G_i \]由此可以得出一个有趣的结论:\(F_{\log n}\) 为最小生成树。
接下来看操作
insert( e = ( v, w ) )
表示 \(v\) 到 \(w\) 连一条有向边。下文中的 Graph
表示 \(G_{\log n}\),是一个 std::set
的数组,作用类似邻接表。
F[ i ]
表示 \(F_i\), 是一个 ETT 的森林(你最好封装一下,否则你会疯掉的)。
上代码~(惊不惊喜,是伪代码!)
Graph[ v ]. insert( e ), Graph[ w ]. insert( e )
e. level = log n
if ( ! F[ log n ]. query( e ) )
F[ log n ]. insert( e )
query( v, w )
不用解释。
return F[ log n ]. findrt( v ) == F[ log n ]. findrt( w )
delete( e = ( v, w ) )
实际上的程序比这个复杂……
Graph[ v ]. erase( e ), Graph[ w ]. erase( e )
if ( e \in F[ log n ] )
for i = e. level to log n do
F[ i ]. delete( e )
for i = e. level to log n do
令 Tv = F[ i ] 中有 v 的那棵树,Tw = F[ i ] 中有 w 的那棵树
if ( Tv. size > Tw.size )
swap( Tv, Tw ) // 你可以理解 Tv 与 T_w 为指针,所以可以交换。
for each 第 i 层中的边 e' = ( x, y )
if ( x \in F[ v ] )
if ( y \in Tw )
F[ i ]. insert( e' )
return
else
if (y \in Tb )
e'. level = i - 1
来解释一下为什么中间一些东西是对的。
因为 \(|F_i| \le 2^i\),\(T_v, T_w \in F_i\)
所以 \(|T_v| + |T_w| \le 2^i\)
因为 \(|T_v| \le |T_w|\)
所以 \(|T_v| \le 2^{i - 1}\)
这个东西的复杂度是 \(O(log ^2 n)\),是 delete 的一个 log 和 ETT 的一个 log。