Loading

【题解】CF708C - Centroids

题目大意

题目链接

定义一棵树的重心结点 \(u\) 为该树中任意满足删除该结点和与其相连的边后,产生的若干连通块大小均不超过 \(\lfloor \frac{n}{2} \rfloor\) 的结点。现在给出一棵包含 \(n\) 个结点和 \(n - 1\) 条边的树。每次您可以在原树上删去一条边再连上一条边,使得该树仍然保持树的形态。请问在结点 \(1\) 到结点 \(n\) 中有多少结点可以通过操作变成操作后树的重心?按升序输出结点编号。

时限 \(4\) 秒,空间限制 \(500\ MB\)\(2 \leq n \leq 400000\)

解题思路

这道题是一道很好的 树形 dp 。具体来说,我们用换根 dp 来优化我们的贪心思路。如果您想到了暴力的贪心思路但无法优化,因此点开了本篇题解,鉴于题目难度,属于正常现象(。

我们首先抓住题目中的关键信息。题面很短,我们应该抓住这个操作来思考。这个操作的本质是什么?从题面来看,我们可以删去一条边 \((u, v)\) ,再连接一条边 \((x, y)\) 。因为删去 \((u, v)\) 后,原树中以 \(v\) 为根的子树会变成独立的连通块。如果想要保持树的形态,我们应该从这个连通块中连出一条向原树剩余部分的边,所以 \((x, y)\) 一定是由一个在子树 \(v\) 中的结点和不在子树 \(v\) 中的结点组成的。实际上,这个操作相当于从树中选出任意一个结点 \(u\) ,把 \(u\) 的子树移植到原树中的任何一个位置。

完成了操作的转化,我们来思考它的作用。我们可以发现一个显然的性质,如果一个结点 \(u\) 在原树中是树的重心,那么经过一次操作,它仍然可以是树的重心。所以我们只需要讨论不是原树重心的结点。我们应该需要用上面的操作来尽量令它们变成重心,并且操作失败的结点一定不是树的重心。

对于不是原树重心的结点 \(u\),删去它后应该有两种情况:

  1. \(u\) 的某一棵子树在独立后大小大于 \(\lfloor \frac{n}{2} \rfloor\)

  2. 原树中除 \(u\) 的子树以外的部分大小大于 \(\lfloor \frac{n}{2} \rfloor\)

我们还可以发现一个显然的性质:大小大于 \(\lfloor \frac{n}{2} \rfloor\) 的连通块最多只有 \(1\) 个,也就是上面的两种情况之一。

我们考虑贪心。我们期望的结果是最大的连通块大小 \(\leq \lfloor \frac{n}{2} \rfloor\) ,所以我们可以从大小大于 \(\lfloor \frac{n}{2} \rfloor\) 的连通块中取出一棵子树,把这棵子树接在结点 \(u\) 的子结点处,使得原本最大的连通块的大小 \(\leq \lfloor \frac{n}{2} \rfloor\)。可以证明,把这棵子树移植到别的位置一定不会更优。这样,我们可以得到这棵子树的取值范围:设最大的连通块大小为 \(k\),则这棵子树的取值范围在 \([k - \lfloor \frac{n}{2} \rfloor, \lfloor \frac{n}{2} \rfloor]\) 之间。

于是,我们可以把问题转化成一个更本质的问题,而且包括上面分的两种情况:对于一个不是原树重心的结点 \(u\) ,是否存在一棵以结点 \(v\) 为根的子树,使得将 \(v\) 和其子树变成 \(u\) 的子结点时,删掉 \(u\) 后最大的连通块大小 \(\leq \lfloor \frac{n}{2} \rfloor\) 。对于这个模型,我们考虑用树形 dp 和换根 dp 来维护取值范围内最大的子树大小。

容易想到的是,对于上面分出的第二种情况,我们可以维护一个值 \(f_{u, 0}\) 表示 以 \(u\) 为根的子树中在取值范围内且最大的子树大小和重儿子。我们再维护以 \(u\) 为根的子树大小,分别记为 \(t_u\)\(s_u\)。如果 \(t_{s_u} > \lfloor \frac{n}{2} \rfloor\) ,最优的解决方案是从 \(s_u\) 的子树剔出一棵大小在取值范围内且最大的子树,符合 \(f_{s_u}\) 的定义。因此,对于第二种情况,我们直接判断 \(t_{s_u} - f_{s_u}\) 是否小于等于 \(\lfloor \frac{n}{2} \rfloor\) 即可。

对于第一种情况,我们需要从原树中 \(u\) 子树以外的部分选出一棵取值范围内的极大子树,显然难以用已维护的值来判断。我们考虑维护一些新值来维护。定义 \(h_u\) 为原树除子树 \(u\) 的部分中,在取值范围内的极大子树大小。\(h_u\) 无法用正常的树形 dp 维护,考虑使用换根 dp 。

因为换根 dp 的更新顺序,我们考虑 \(h\) 从父结点 \(u\) 转移到其某一子结点 \(v\) 。假如原树中剔除子树 \(v\) 后,剩余部分的大小 \(\leq \lfloor \frac{n}{2} \rfloor\) ,说明剩余部分是取值范围内的极大值;否则,因为 \(h_u\) 满足大小限制,从 \(h_u\) 转移。接着考虑结点 \(u\) 的其他子树,此时需要分类讨论。定义子树 \(u\) 中子树大小为 \(f_{u, 0}\) 的结点为 \(g_u\) 。若 \(g_u \neq v\) ,说明我们可以直接取 \(f_{u, 0}\) ;否则,我们需要取次大值,记为 \(f_{u, 1}\) ,和 \(f_{u, 0}\) 一起维护即可。最终的 \(h_u\) 在合法的转移中取最大值。

由此,我们可以推导出第一种情况的成立条件。设 \(k = n - t_u\)\(k > \lfloor \frac{n}{2} \rfloor\) ,如果 \(k - h_u \leq \lfloor \frac{n}{2} \rfloor\) ,说明 \(u\) 可以在一次操作后变成新树的重心,否则不能。

总时间复杂度 \(O(n)\) ,可以通过 \(n \leq 400000\) 的数据。

参考代码

#include <cstdio>
#include <algorithm>
using namespace std;

const int maxn = 4e5 + 5;
const int maxm = 1e6 + 5;

struct node {
	int to, nxt;
} edge[maxm];

int n, cnt;
int head[maxn], f[maxn][2], g[maxn];
int h[maxn], son[maxn], ans[maxn], size[maxn];

void add_edge(int u, int v) {
	cnt++;
	edge[cnt].to = v;
	edge[cnt].nxt = head[u];
	head[u] = cnt;
}

void dfs1(int u, int fa) {
	int val;
	size[u] = 1;
	for (int i = head[u]; i; i = edge[i].nxt) {
		if (edge[i].to == fa) {
			continue;
		}
		dfs1(edge[i].to, u);
		size[u] += size[edge[i].to];
		if (size[edge[i].to] > size[son[u]]) {
			son[u] = edge[i].to;
		}
		if (size[edge[i].to] <= n / 2) {
			val = size[edge[i].to];
		} else {
			val = f[edge[i].to][0];
		}
		if (val > f[u][0]) {
			f[u][1] = f[u][0];
			f[u][0] = val;
			g[u] = edge[i].to;
		} else if (val > f[u][1]) {
			f[u][1] = val;
		}
	}
}

void dfs2(int u, int fa) {
	ans[u] = 1;
	if (n - size[u] > n / 2 && n - size[u] - h[u] > n / 2) {
		ans[u] = 0;
	} else if (size[son[u]] > n / 2 && size[son[u]] - f[son[u]][0] > n / 2) {
		ans[u] = 0;
	}
	for (int i = head[u]; i; i = edge[i].nxt) {
		if (edge[i].to == fa) {
			continue;
		}
		if (n - size[u] <= n / 2) {
			h[edge[i].to] = n - size[u];
		} else {
			h[edge[i].to] = h[u];
		}
		if (g[u] == edge[i].to) {
			h[edge[i].to] = max(h[edge[i].to], f[u][1]);
		} else {
			h[edge[i].to] = max(h[edge[i].to], f[u][0]);
		}
		dfs2(edge[i].to, u);
	}
}

int main() {
	int u, v;
	scanf("%d", &n);
	for (int i = 1; i <= n - 1; i++) {
		scanf("%d%d", &u, &v);
		add_edge(u, v);
		add_edge(v, u);
	}
	dfs1(1, 0);
	dfs2(1, 0);
	for (int i = 1; i <= n; i++) {
		printf("%d ", ans[i]);
	}
	puts("");
	return 0;
}
posted @ 2021-07-24 23:41  kymru  阅读(143)  评论(0编辑  收藏  举报