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;
}
posted @ 2022-03-07 11:23  GaryH  阅读(32)  评论(0编辑  收藏  举报