【题解】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\),删去它后应该有两种情况:
-
\(u\) 的某一棵子树在独立后大小大于 \(\lfloor \frac{n}{2} \rfloor\)
-
原树中除 \(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;
}