AGC027F 题解
AGC027F 题解
看到题解里都是 \(O(TN^3)\) 或 \(O(TN^4)\) 复杂度的做法,那我就来发一个 \(O(TN^2)\) 的做法。
题意:
定义一棵树上的一次操作为,选择树上的一个未被选择过的叶子节点,找到其唯一的出边并断掉,
再把该点与任意一个节点连一条新边。
\(T\) 组数据,每次给两棵 \(N\) 个节点的树 \(A,B\),问至少多少次操作可以将 \(A\) 变成 \(B\),或报告无解。
\(N\le50,T\le20\)。
做法:
我们先分类讨论答案的取值。
如果答案是 \(0\),那 \(A\) 和 \(B\) 必然一模一样,这种情况先处理掉。
如果答案不超过 \(N-1\),那 \(A\) 中必有至少一个点没有操作过,我们枚举这个点,记为 \(r\),
并把 \(r\) 作为两棵树上的根节点。
记点 \(u\) 在 \(A\) 树中的邻域为 \(a(u)\),在 \(B\) 树中的的邻域为 \(b(u)\),
我们在 \(A\) 和 \(B\) 两棵树中,找到点 \(r\) 和一个包含点 \(r\) 的极大点集 \(S\),满足:
\(S\) 在两棵树中都是联通的,且有 \(\forall u\in S,a(u)=b(u)\),即连通块 \(S\) 在两棵树里形状完全一样。
容易发现,这样的点集 \(S\) 是唯一的。
那么,我们断言,此时我们一定不会对 \(S\) 中的点进行操作,因为:
如果操作了 \(S\) 中的某点 \(u\),则操作后 \(u\) 的父亲一定不是 \(B\) 树中 \(u\) 的父亲,则还需将 \(u\) 的父亲改回来,
而点 \(u\) 已经操作过了一次,那么其父亲也不能再操作,故一定不合法。
上面的性质同样启示我们,如果点 \(u\ne r\),且 \(u\) 在 \(A\) 树中的父亲与其在 \(B\) 树中的父亲不同,
则我们必须操作恰 一次 \(u\),且一定会把 \(u\) 接到其在 \(B\) 树上的父亲上。
那我们现在考虑,只操作不在 \(S\) 中的点,容易发现每次操作一定形如以下形式:
每次会在 \(A\) 树中选择一个叶子节点 \(u\),满足此时 \(B\) 树上点 \(u\) 不在 \(S\) 中且与 \(S\) 中点有恰一条边相连。
我们找到这个 \(S\) 中与 \(u\) 相连的点 \(v\),显然点 \(v\) 是 \(B\) 树上点 \(u\) 的父亲,也就是 \(u\) 的目标父亲。
我们此时在 \(A\) 树中把 \(u\) 接到 \(v\) 上,并将 \(u\) 放入点集 \(S\),就完成了一次合法的操作。
故原问题有解,当且仅当存在一种符合以上形式的操作顺序,
使每个初始时不在 \(S\) 中的点都被选择并操作了一次,即除了不动点,每个点的父亲都变成了目标父亲。
那么,唯一的问题,就是该如何确定依次选点的顺序,使得在无法再选任何点时,已选点数最大。
我们发现,其实任何一种合法的策略都是最优的,这个可以用决策包容性证明,即:
考虑每找到一个可以选的点 \(u\),我们一定会贪心地操作点 \(u\),使得 \(u\) 的父亲变为其在 \(B\) 树上的目标父亲。
为什么?我们考虑,此时操作点 \(u\),会让操作后 \(A\) 树中某些本来不是叶子节点的点变成叶子节点,
而 \(A\) 树中本来就是叶子节点的点也不会在操作后变成非叶子节点。
同时,我们把 \(u\) 加入集合 \(S\) 后,也会使一些原来不与 \(S\) 中点有边相连的点,变得与 \(S\) 中有边相连,
那些本来就与 \(S\) 中有边相连的点,也不会因为这次操作而变得不与 \(S\) 中的点有边相连。
也就是说,假设 \(T\) 代表了当前可以操作的点集,\(T'_u\) 代表了操作了点 \(u\) 后的新的可操作点集,则:
我们必然有 \(S\cup T\subseteq S\cup\{u\}\cup T'_u\),故我们贪心的操作一定不劣。
那么,我们只要每次枚举每一个点,判断这个点是否可以操作,如果可以操作就直接操作后再枚举,
直到有一次枚举了所有点后都无法再继续操作新点,此时检查所有点是否都有合法父亲即可。
上面是一种单组数据复杂度 \(O(N^3)\) 的做法。其实我们还有 \(O(N^2)\) 的做法,
就是我们维护当前 \(A\) 树中的叶子节点集合 \(P\),以及当前 \(B\) 树中与 \(S\) 有边相连的点集 \(Q\),
显然,当前可以操作的点集就等于 \(P\cap Q\)。
那么,我们可以将 \(P\) 和 \(Q\) 状压到两个数里,每次 \(O(1)\) 地找到 \(P\cap Q\) 这个数的任意一个值为 \(1\) 的位,
这个位就对应了一个当前可以操作的点,我们对该点进行操作并维护其对 \(P\) 和 \(Q\) 带来的影响,
影响具体为:假设我们操作了点 \(u\),我们会把 \(u\) 从 \(P\) 和 \(Q\) 里删除,
并检查 \(A\) 树中 \(u\) 的父亲是否可以加入 \(P\),以及 \(B\) 树中 \(u\) 的所有儿子是否可以加入 \(Q\)。
显然,所有 \(u\) 对 \(P\) 和 \(Q\) 带来的总影响是 \(O(N)\) 的,故加上枚举的不动点 \(r\),总复杂度就是 \(O(N^2)\) 的。
但是,这种方法的扩展性较弱,因为整数的大小有一个 \(2^{64}\) 的限制,
如果用这种方法解决 \(N\le5\times10^3\) 的话,我们就只能用 bitset
来代替整数去维护点集,
这样的复杂度就变成了 \(O(\frac{N^3}{w})\),瓶颈在于求两个 bitset
的交,以及取出 bitset
里某个值为 \(1\) 的位。
但是,我们实际上可以动态的维护两个 bitset
的交,并动态的维护 bitset
中所有值为 \(1\) 的位,
具体来说,我们开一个新的 bitset
,用于记录 \(P\cap Q\) 的状态,
同时维护一个队列 \(q\) 记录 \(P\cap Q\) 中所有值为 \(1\) 的位,每个点在操作后也会对这些东西造成影响,
但由于所有点对 \(P\) 和 \(Q\) 造成的总影响是 \(O(N)\) 的,故对 \(P\cap Q\) 造成的总影响也是 \(O(N)\) 的,
而对于队列 \(q\),每个元素只会进入队列最多一次,所以对队列 \(q\) 的影响也是 \(O(N)\) 的。
这时的时间复杂度就变成了 \(O(N^2)\) 的,
实现时可以用数组代替 bitset
,因为这种方法没有用到 bitset
的任何特殊性质。
但是,我们还有最后一种特殊的情况,即如果答案等于 \(N\),那图中就不存在不动点时该怎么办。
这时,我们考虑枚举第一步操作,来人为的创造不动点。
具体来说,若第一步操作中,我们把 \(u\) 接到了 \(v\) 上,后续我们还有 \(N-1\) 步操作需要完成,
那么我们一定还会对 \(v\) 进行一次操作,故我们需要把除了 \(u\) 和 \(v\) 的所有点都接到 \(u\) 后面,
最后再将 \(v\) 接到其应该在的位置上。
这时的问题就和答案不超过 \(N-1\) 时的模式类似了,因为在第一次操作后,\(u\) 就成为了不动点,
我们就可以用同样的方法解决问题,而这样的时间复杂度是 \(O(N^3)\) 的,
因为我们在考虑第一步操作时枚举了两个点。
但仔细一想,你会发现,其实在第一步操作中,枚举 \(v\) 是不必要的,
因为我们可以只枚举 \(u\),并把 \(A\) 当成无根树,并在 \(B\) 树中以 \(u\) 为根,且此时集合 \(S\) 中只有 \(u\) 一个点。
我们对新的无根树 \(A\),以及有根树 \(B\),做和答案不超过 \(N-1\) 时类似的做法,
如果这样做后,\(S\) 中有 \(N-1\) 个点,那我们就把那个不在 \(S\) 中的点作为 \(v\),就一定是合法的。
而如果无法使 \(S\) 的大小成为 \(N-1\),则我们一定找不到一组答案为 \(N\) 的解,
因为我们做的每一步操作都具有决策包容性,即可以认为我们的每一步抉择都是足够优的。
那么,这样的时间复杂度也就变成了单组 \(O(N^2)\) 的,
正常实现的代码是洛谷最优解第二,也许卡一下常能到最优解第一。
代码有一些细节(也可能是我写的比较丑),放出来以供参考。
#include <bits/stdc++.h>
#define fi first
#define se second
#define vi vector
#define db double
#define mp make_pair
#define pb push_back
#define LL long long
#define emp emplace_back
#define pii pair < int , int >
#define SZ(x) ((int)(x.size()))
#define all(x) x.begin(), x.end()
#define ckmax(a, b) ((a) = max((a), (b)))
#define ckmin(a, b) ((a) = min((a), (b)))
#define rep(i, a, b) for (int i = (a); i <= (b); i++)
#define per(i, a, b) for (int i = (a); i >= (b); i--)
#define edg(i, v, u) for (int i = head[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to)
using namespace std;
int read (char ch = 0, int x = 0, int f = 1) {
while (ch < '0' || ch > '9') f = ch == '-' ? -1 : 1, ch = getchar();
while (ch >= '0' && ch <= '9') x = x * 10 + ch - 48, ch = getchar();
return x * f;
}
const int N (55);
int n;
int sum;
int res;
int fa[N];
int deg[N];
int ins[N];
int conn[N];
int mska[N];
int mskb[N];
int ga[N][N];
vi < int > A[N];
vi < int > B[N];
queue < int > q;
void dfsA (int u, int ff, int o = 0) {
fa[u] = ff, deg[ff]++;
for (int v : A[u]) if (v ^ ff) dfsA (v, u), o = 1;
if (!o) mska[u] = 1;
}
void dfsB (int u, int ff, int o = 1) {
if (o) sum++, conn[u] = 1;
for (int v : B[u]) if (v ^ ff) {
if (o && !ga[u][v]) mskb[v] = 1;
dfsB (v, u, o & ga[u][v]);
}
}
void work() {
n = read();
rep (i, 1, n) {
A[i].clear(), B[i].clear();
rep (j, 1, n) ga[i][j] = 0;
}
rep (i, 2, n) {
int u = read(), v = read();
ga[u][v] = ga[v][u] = 1, deg[u]++, deg[v]++;
A[u].pb (v), A[v].pb (u);
}
rep (i, 2, n) {
int u = read(), v = read();
deg[u]--, deg[v]--;
B[u].pb (v), B[v].pb (u);
}
int ok = 1;
rep (i, 1, n) if (deg[i] != 0) ok = 0, deg[i] = 0;
if (ok) return puts ("0"), void();
int ans = -1;
rep (r, 1, n) {
sum = res = 0;
while (!q.empty()) q.pop();
rep (i, 1, n) mska[i] = mskb[i] = conn[i] = deg[i] = fa[i] = ins[i] = 0;
dfsA (r, 0), dfsB (r, 0);
rep (i, 1, n) if (mska[i] && mskb[i])
q.push (i), ins[i] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
conn[u] = 1, res++;
ins[u] = mska[u] = mskb[u] = 0;
for (int v : B[u]) if (!conn[v]) {
mskb[v] = 1;
if (mska[v] && !ins[v])
q.push (v), ins[v] = 1;
}
deg[fa[u]]--;
if (!deg[fa[u]] && !conn[fa[u]]) {
mska[fa[u]] = 1;
if (mskb[fa[u]] && !ins[fa[u]])
q.push (fa[u]), ins[fa[u]] = 1;
}
}
if (res + sum == n) {
if (ans == -1) ans = res;
else ckmin (ans, res);
}
}
rep (r, 1, n) if (SZ (A[r]) == 1) {
sum = res = 0;
while (!q.empty()) q.pop();
rep (i, 1, n) mska[i] = mskb[i] = conn[i] = deg[i] = fa[i] = ins[i] = 0;
for (int v : B[r]) mskb[v] = 1;
rep (u, 1, n) for (int v : A[u]) deg[v]++;
for (int v : A[r]) deg[v]--;
rep (i, 1, n) if (deg[i] == 1) {
mska[i] = 1;
if (mskb[i] && !ins[i]) q.push (i), ins[i] = 1;
}
conn[r] = sum = 1;
while (!q.empty()) {
if (sum == n - 1) break;
int u = q.front(); q.pop();
conn[u] = 1; sum++;
ins[u] = 0;
mska[u] = mskb[u] = 0;
for (int v : B[u]) if (!conn[v]) {
mskb[v] = 1;
if (mska[v] && !ins[v])
ins[v] = 1, q.push (v);
}
for (int v : A[u]) if (!conn[v]) {
deg[v]--;
if (deg[v] == 1) {
mska[v] = 1;
if (mskb[v] && !ins[v])
q.push (v), ins[v] = 1;
}
}
}
if (sum == n - 1) {
int o = 0;
rep (i, 1, n) if (!conn[i]) o = i;
if (o && SZ (B[o]) == 1) {
if (ans == -1) ans = n;
else ckmin (ans, n);
}
}
}
printf ("%d\n", ans);
}
int main() {
int tasks = read();
while (tasks--) work();
return 0;
}