飘花效果

记一个动态加边数桥问题

RT 啊,并非动态加边动态询问两点是否一个 e-bcc 有几条必经边之类的,纯如题:

给定 \(n\) 个点的图,初始没有任何边。按照顺序加入 \(m\) 条无向边边 \((u ,v)\),求每次加边后该图的割边(桥)的数量。
点的编号为 \(0 \sim n - 1\)
\(1\le n \le 5\times 10^4\)\(1\le m\le 10^5\)

这是 nfls 里的一道作业,理解我比较长的时间,遂记录一下,顺便帮助后人理解

割边容易想到边双缩点,此时是一棵树或一个森林,方便我们分析。

考虑分类讨论对答案的影响:

图中,圆圈内部是边双连通分量,红边是割边,我们考虑新加一条边:

如图,我们新加了一条绿边,由于该边在同一个边双内部,因此对答案无影响。

实际编写代码时,我们可以写函数 finde(x) 表示寻找 \(x\) 节点所在边双,为了方便起见,我们不妨不记录边双编号,而是找这个边双内的任意一个点,作为代表元

// 类似并查集的找祖先,我们寻找该边双的代表元。
inline int finde(int u) { return fae[u] == u ? u : fae[u] = finde(fae[u]);}

那么这种情况对应代码:

int u = read() + 1 ,v = read() + 1; // 题目 0-index。
int eu = finde(u) ,ev = finde(v);
if(eu == ev) { // 同属一个边双,割边数量不增加。
    writeln(ans) ; continue; 
}

接下来是第二种情况:

如图,我们增加一条蓝边,它两端属于两个不同的边双。

那么新的边双数是多少呢?

我们发现存在一个紫色的环,此时新的边双数量为 \(2\)

也就是说,对于原始该环上的所有割边都会变成非割边

我们只要查的 eu = finde(u)ev = finde(v),那么边双缩点后的树中 \(eu \rightsquigarrow ev\) 的割边贡献都为减一。

只要我们想办法使树高固定在 \(\mathcal O(\log n)\) 以内,暴力删除贡献就是可行的。

注意到第一种情况啥都不改,目前只有这种情况会产生影响并合并点。

对于合并 \(x\) 个边双,一次操作会减少 \(x - 1\) 个边双,最坏复杂度 \(\mathcal O(x)\),总共至多 \(n\) 个边双,均摊下来复杂度是 \(\mathcal O(n)\)

inline void merge_path(int u ,int v) { // 合并 u ,v 所属边双为一个新的边双。
// pa 是边双树上的父亲。
    int lca = 0 ; ++ idx ; // idx 用来标记路径。
    vec1.clear() ; vec2.clear(); // vec1 ,vec2 用来记录 u -> lca ,v -> lca 的路径。
    while(!lca) {
        if(u) {
            u = finde(u);
            vec1.push_back(u);
            if(vis[u] == idx) { lca = u ; break ; }
            vis[u] = idx ,u = pa[u];
        } if(v) {
            v = finde(v);
            vec2.push_back(v);
            if(vis[v] == idx) {lca = v ; break;}
            vis[v] = idx ,v = pa[v];
        }
    }
    for(int i : vec1) { // 删除贡献,注意 vec1 / vec2 中存有 u ,v。
        fae[i] = lca ; 
        if(i == lca) break;
        -- ans ;
    }
    for(int i : vec2) {
        fae[i] = lca ;
        if(i == lca) break;
        -- ans; 
    }
}

然而还有另外一种情况,因为边双缩点后可能形成森林。

如图,我们添加橙边,显然要把两个森林合并为一棵树,朴素的想法是把 \(5 ,7\) 连边即可。

但这样复杂度可能退化为 \(\mathcal O(n^2)\),并且对于 \(1 ,2 ,3 ,4\) 这些节点,查询 finde 会返回 1 而非 6

考虑时间复杂度,我们采用启发式合并,用小的合并大的,上图中就是左右互换。

对于后面的问题,我们只要把 \(7\) 提起作根即可。

启发式合并的树高均摊是 \(\mathcal O(\log n)\) 的,因此总时间复杂度均摊为 \(\mathcal O(m \log n)\)

这个提根有点类似于 splay 呢,splay 用 zig、zag 和提根均摊,这里用启发式合并均摊。

判断是否同一连通块(情况二、三)用并查集即可(数组为 fa)。

inline void makeroot(int u) { // 把 u 提起作根。
// pa 是边双树上的父亲。
    u = finde(u);
    int rt = u ,child = 0 ;
    while(u) {
        int f = pa[u];
        pa[u] = child ,fa[u] = rt ,child = u ,u = f; // 只需要修改顶到节点的链即可。
    } sz[rt] = sz[child]; // 我们只关心代表元,代表元的大小反应的是该边双树的大小。
}

总代码:

inline int finde(int u) { return fae[u] == u ? u : fae[u] = finde(fae[u]);}
inline int find(int u) { return u == fa[u] ? u : fa[u] = find(fa[u]) ;}
inline void makeroot(int u) {
    u = finde(u);
    int rt = u ,child = 0 ;
    while(u) {
        int f = pa[u];
        pa[u] = child ,fa[u] = rt ,child = u ,u = f;
    } sz[rt] = sz[child];
}
inline void merge_path(int u ,int v) {
    int lca = 0 ; ++ idx ;
    vec1.clear() ; vec2.clear();
    while(!lca) {
        if(u) {
            u = finde(u);
            vec1.push_back(u);
            if(vis[u] == idx) { lca = u ; break ; }
            vis[u] = idx ,u = pa[u];
        } if(v) {
            v = finde(v);
            vec2.push_back(v);
            if(vis[v] == idx) {lca = v ; break;}
            vis[v] = idx ,v = pa[v];
        }
    }
    for(int i : vec1) {
        fae[i] = lca ; 
        if(i == lca) break;
        -- ans ;
    }
    for(int i : vec2) {
        fae[i] = lca ;
        if(i == lca) break;
        -- ans; 
    }
}
namespace lolcrying{

	signed main(){

        n = read() ,m = read();
        up(i ,1 ,n) vis[i] = pa[i] = fa[i] = fae[i] = 0 ,sz[i] = 1;
        up(i ,1 ,n) fa[i] = fae[i] = i ; 
        ans = idx = 0 ;
        
        up(i ,1 ,m) {
            int u = read() + 1 ,v = read() + 1;
            int eu = finde(u) ,ev = finde(v);
            if(eu == ev) {
                writeln(ans) ; continue; 
            }
            int fau = find(u) ,fav = find(v);
            if(fau ^ fav) {
                ++ ans;
                if(sz[fau] > sz[fav]) swap(eu ,ev) ,swap(fau ,fav) ,swap(u ,v);
                makeroot(u);
                pa[u] = ev ,fa[fau] = fav ,sz[fav] += sz[fau];
            } else merge_path(u ,v);
            writeln(ans);
        }
		return 0 ;
	}
}
posted @ 2026-08-03 22:47  2021zjhs005  阅读(6)  评论(0)    收藏  举报