【题解】P4228 [清华集训2017] 榕树之心

link

题意

给定一棵有根树,初始时核心在 1 1 1 处, 1 1 1 为标记点。每次操作选择一个标记点的邻接点,将其标记,将核心往该点的方向移动一步。求 n − 1 n-1 n1 次操作后,核心是否可能落在 i i i 上。 n ≤ 1 0 5 n\le 10^5 n105

题解

观察发现,一些子树内部可以两两抵消。即操作若干次后会回到根节点。当核心在 i i i 上,两个节点分属于点 i i i 的不同子树时,选择这两个节点后,核心会回到 i i i

考虑子任务 3,即只求根节点的答案。

f i f_i fi 表示仅考虑 i i i 的子树,操作完子树后,核心离 i i i 的最近距离。

对于点 i i i,仅考虑其子树,最优策略是用剩下的子树与重儿子两两配对消除。若不能完全消除,其正确性显然。若可以完全消除,有最优策略:每次将重儿子中的点与其它子树中深度最大的点配对,剩下的点两两配对。这样 f i f_i fi 不超过 1 1 1

设点 i i i 的重儿子为 s s s,点 i i i 的子树大小为 s z i sz_i szi

考虑转移:

  • 若重儿子可以被完全消除,即 f s + 1 ≤ s z i − s z s − 1 f_s+1\le sz_i-sz_s-1 fs+1sziszs1,此时核心离 i i i 的距离为 f i + 1 f_i+1 fi+1,剩下的子树有 s z i − s z s − 1 sz_i-sz_s-1 sziszs1 个点,有 f i = ( s z i − 1 )   m o d   2 f_i=(sz_i-1)\bmod 2 fi=(szi1)mod2
  • 若重儿子不能被完全消除,有 f i = f s + 1 − ( s z i − s z s − 1 ) f_i=f_s+1-(sz_i-sz_s-1) fi=fs+1(sziszs1)

此时得到了根节点的答案。

接下来考虑换根 dp。

从根节点到点 i i i 的路径上的点看作已经选过了,直接无视。仅需要考虑它们的子树的影响。

对于路径上的节点,将它们的子树当作 i i i 的子树考虑。于是在换根的过程中,只需要多记录一下路径上的重儿子即可。

但是,当路径上的点同时又是重儿子时,会发生冲突。选次重即可。

考虑正确性。当我们将这些点两两配对的时候,假设它们是 ( u , v ) (u,v) (u,v) d e p u < d e p v dep_udepu<depv,那么在路径跑到 v v v 的父亲处执行这对操作可以抵消。

实现

注意算答案时的重儿子与向下换根的重儿子是有一点不一样的。

#include 
using namespace std;
const int N = 100005;
int type, T, n, cnt = 0, fir[N], nxt[N << 1], to[N << 1], fa[N], f[N], s1[N], s2[N], sz[N], ans[N];
template<typename Ty> void read(Ty &x) {
	int c = getchar(), f = 1;
	for (; c < '0' || c > '9'; c = getchar()) if (c == '-') f = -1;
	for (x = 0; c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) + (c ^ 48);
	x *= f;
}
void ade(int u, int v) {
	cnt++, nxt[cnt] = fir[u], fir[u] = cnt, to[cnt] = v;
	cnt++, nxt[cnt] = fir[v], fir[v] = cnt, to[cnt] = u;
}
void dfs1(int r) {
	sz[r] = 1;
	for (int i = fir[r]; i; i = nxt[i])
		if (to[i] != fa[r]) {
			fa[to[i]] = r, dfs1(to[i]), sz[r] += sz[to[i]];
			if (sz[to[i]] >= sz[s1[r]]) s2[r] = s1[r], s1[r] = to[i];
			else if (sz[to[i]] > sz[s2[r]]) s2[r] = to[i];
		}
	if (sz[r] == 1) f[r] = 0;
	else if (f[s1[r]] + 1 <= sz[r] - sz[s1[r]] - 1) f[r] = (sz[r] - 1) & 1;
	else f[r] = f[s1[r]] + 1 - (sz[r] - sz[s1[r]] - 1);
}
int szmx(int x, int y) { return sz[x] > sz[y] ? x : y; }
void dfs2(int r, int mx, int sum) {//mx为路径上的最重儿子,sum表示路径上的子树总和
	if (r != 1) {
		int siz = sz[r] + sum, tmp = szmx(mx, s1[r]);//这里一定要再开一个变量tmp,否则可能会和下面的路径最重儿子mx冲突
		if (f[tmp] + 1 <= siz - sz[tmp] - 1) ans[r] = (siz - 1) & 1;
		else ans[r] = f[tmp] + 1 - (siz - sz[tmp] - 1);
	}
	for (int i = fir[r], v; i; i = nxt[i])
		if (to[i] != fa[r]) {
			if (to[i] == s1[r]) v = s2[r];
			else v = s1[r];
			dfs2(to[i], szmx(mx, v), sum + sz[r] - sz[to[i]] - 1);
		}
}
int main() {
	read(type), read(T);
	while (T--) {
		memset(fir, 0, sizeof(fir)), memset(nxt, 0, sizeof(nxt)), cnt = 0, memset(s1, 0, sizeof(s1)), memset(s2, 0, sizeof(s2));
		read(n);
		for (int i = 1, u, v; i < n; i++) read(u), read(v), ade(u, v);
		dfs1(1), ans[1] = f[1];
		if (type == 3) { printf("%d\n", ans[1] == 0); continue; }
		dfs2(1, 0, 0);
		for (int i = 1; i <= n; i++) printf("%d", ans[i] == 0);
		printf("\n");
	}
	return 0;
}

启示

  • 像这样奇怪的树上移动问题,有可能跟重儿子相关,可以考虑将重儿子与其它子树做区分
  • 树上的移动问题,可以考虑换根 dp,要看到换根前后问题的统一性,考虑用同样的思路解决问题

你可能感兴趣的:(题解,算法)