洛谷 CF1284F. New Year and Social Network

观察样例,猜测答案为 $n-1$。

引理:Hall 定理
设二分图左部点集合为 $X$,右部点集合为 $Y$,则存在完美匹配的充分必要条件是:对于 $X$ 中任意 $k$ 个不重复的点,与 $Y$ 中至少 $k$ 个点相邻。

显然本题中 $X$ 是 $T_1$ 的边集,$Y$ 是 $T_2$ 的边集。
那么枚举每个 $S \in X$,考虑删除 $S$ 集合的边能否从 $T_2$ 中找到对应的匹配。
对于 $T_2$ 中的一条边 $(u,v)$,可以匹配需要满足 $T_1$ 中 $u,v$ 不连通,即所处连通块不同。
考虑每个连通块的贡献。对于大小为 $t$ 的连通块它在 $Y$ 中最多只有 $t-1$ 条边是不能匹配的。
那么 $Y$ 中不能匹配的边数最多就是 $\sum t-1 = n - |S|$,因此最少有 $|S|$ 条边可以匹配上 $S$ 中删除的边。
存在完美匹配,答案一定为 $n-1$。


考虑暴力地模拟这个匹配过程,每次枚举 $T_1$ 中任意一条边 $(u,v)$,找到 $T_2$ 中的 $(x,y)$ 与之匹配。
即需要满足 $T_1$ 中,$x,y$ 在删掉 $(u,v)$ 之后不在同一个连通块;在 $T_2$ 中,$x,y$ 在 $(u,v)$ 路径上;$u,v$ 在 $T_1$ 相邻,$x,y$ 在 $T_2$ 相邻。
满足 $x,y$ 在 $(u,v)$ 路径上是为了删除 $x,y$ 之后 $T_2$ 仍然连通,方便转化为子问题。
这样的 $x,y$ 总是存在。把 $T_2$ 上 $u \to v$ 的路径在 $T_1$ 上画出来,至少有一次跨越了 $u,v$,所以一定存在 $x,y$。

找到匹配之后,考虑把它缩小成规模为 $n-1$ 的问题。
$T_2$ 中 $(x,y)$ 不能再匹配,可以直接删掉;$T_1$ 中的 $(u,v)$ 虽然不能匹配,但是起到了联通 $T_1$ 路径的作用,所以可以把 $(u,v)$ 这条边缩成一个点。
但是要求 $T_1,T_2$ 点集是对应的,所以理论上 $T_2$ 中 $(u,v)$ 也要缩成一个点。

所以每次枚举 $T_1$ 中 $(u,v)$,在 $T_2$ 中寻找 $(x,y)$,并把两棵树中的 $(u,v)$ 都缩成一个点,$T_2$ 中断开 $(x,y)$。
这样的时间复杂度是 $O(n^2)$。


考虑一个“更优雅”的暴力。每次在 $T_1$ 中选择叶子节点 $u$ 到父亲 $v$的边寻找匹配。
这样的好处在于:$T_2$ 中所有以 $u$ 为其中一个端点的边都能连通 $T_1$ 中的 $u,v$。
那么转化后相当于任意找一个 $T_2$ 中 $u$ 的邻居,把这条边直接和 $T_1$ 的 $(u,v)$ 匹配。

将 $T_1$ 中 $(u,v)$ 合并相当于直接把 $u$ 删掉(是叶子),然后在 $T_2$ 中把这个邻居的边删掉,并在 $u,v$ 连接虚边,注意虚边不能匹配。
所以我们要在 $T_2$ 上维护:找到一条路径上的第一条非虚边、删一条边、加一条虚边。可以直接 LCT 维护。


但我们有更简单的方法:改成删除 $T_2$ 的叶子。
考虑用并查集维护 $T_1$ 中的点是否已经被虚边归属于同一个集合。
那么每次只需要找一个 $T_2$ 中的叶子 $u$ 和它的父亲 $v$,求出它们在 $T_1$ 中的 LCA,设为 $t$。
我们需要找到 $u \to v$ 第一条非虚边,需要分类讨论:

  1. 在 $u \to t$ 路上。即 $u$ 所属连通块深度最浅点深度仍然比 $t$ 大。
  2. 在 $t \to v$ 路上。这需要倍增求出深度最浅的点。

注意对于 $T_1$ 找 $T_2$ 匹配,和对于 $T_2$ 找 $T_1$ 匹配本质是不同的。

#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 5, M = N << 1;
int n;
struct Graph {
    int h[N], e[M], ne[M], idx = 0;
    void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; }
    void addedge(int a, int b) { add(a, b), add(b, a); }
    void build_graph() {
        for (int i = 1; i <= n; i++) h[i] = -1;
        for (int i = 1, u, v; i < n; i++) scanf("%d%d", &u, &v), addedge(u, v);
    }
} G1, G2;

int p[N];
inline int find(int x) { return (p[x] == x) ? x : p[x] = find(p[x]); }
inline void merge(int x, int y) { x = find(x), y = find(y); if (x ^ y) p[x] = y; }

int fa[N][21], dep[N];
void dfs1(int u, int father) {
    fa[u][0] = father, dep[u] = dep[father] + 1;
    for (int i = G1.h[u]; ~i; i = G1.ne[i]) {
        int v = G1.e[i]; if (v == father) continue;
        dfs1(v, u);
    }
}
void init() {
    for (int i = 1; i <= n; i++) p[i] = i;
    for (int i = 1; i <= 19; i++)
        for (int j = 1; j <= n; j++) fa[j][i] = fa[fa[j][i - 1]][i - 1];
}

int Fa[N], in[N];
void dfs2(int u, int father) {
    Fa[u] = father;
    for (int i = G2.h[u]; ~i; i = G2.ne[i]) {
        int v = G2.e[i]; if (v == father) continue;
        dfs2(v, u), in[u]++;
    }
}
int lca(int a, int b) {
    if (a == b) return a;
    if (dep[a] < dep[b]) swap(a, b);
    for (int i = 19; i >= 0; i--)
        if (dep[fa[a][i]] >= dep[b]) a = fa[a][i];
    if (a == b) return a;
    for (int i = 19; i >= 0; i--)
        if (fa[a][i] ^ fa[b][i]) a = fa[a][i], b = fa[b][i];
    return fa[a][0];
}
queue<int> q;

int main() {
    scanf("%d", &n), printf("%d\n", n - 1);
    G1.build_graph(), G2.build_graph();
    dfs1(1, 0), dfs2(1, 0), init();
    for (int i = 1; i <= n; i++)
        if (!in[i]) q.push(i);
    while (q.size()) {
        int u = q.front(); q.pop(); if (u == 1) break;
        int v = Fa[u], t = lca(u, v);
        int w = find(u);   // u 所在连通块深度最浅的点
        if (dep[w] > dep[t]) {
            p[w] = find(fa[w][0]);
            printf("%d %d %d %d\n", w, fa[w][0], u, v);
        } else {
            int now = v;
            for (int i = 19; i >= 0; i--)
                if (dep[fa[now][i]] >= dep[t] && find(fa[now][i]) != w) now = fa[now][i];
            merge(now, fa[now][0]);
            printf("%d %d %d %d\n", now, fa[now][0], u, v);
        }
        if (!(--in[v])) q.push(v);
    }
    return 0;
}
posted @ 2025-03-25 17:20  Conan15  阅读(9)  评论(0)    收藏  举报  来源