洛谷 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$ 第一条非虚边,需要分类讨论:
- 在 $u \to t$ 路上。即 $u$ 所属连通块深度最浅点深度仍然比 $t$ 大。
- 在 $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;
}

浙公网安备 33010602011771号