CSP 2019 树的重心
题解
方法一
一棵根为 (u) 的树的重心,只有可能有两种情况:
- 在 (u) 的重儿子内
- (u) 就是重心
所以可以考虑每回跳重儿子以寻找重心。枚举每条边,现在用一个节点 (u) 代表 (u) 到他父亲的边。
这条边去掉后两边的树为 (u) 的子树和剩下的,包含 (u) 的父亲的那棵树。
首先考虑如何求出 (u) 子树内的重心。考虑倍增 (f_{i,j}) 表示从 (i) 开始跳重儿子,跳 (2^j) 次到哪里。
其次时如何求出 (u) 的父亲的那棵树的重心。还是使用倍增。考虑边换根边修改倍增数组,变化只有 (f_{fa_u,i}) 会产生变化。只是倍增的起点应该是 (u) 的父亲而非根节点,这样每次转换 (u) 时倍增数组和每个节点的重儿子在 (u ightarrow v) 时的变化可以处理。
不过每回 dfs 到一个节点 (v) 回溯得时候要将倍增数组调回 dfs 到 (u) 时的样子。
方法二
考虑统计一个点能作为删掉哪些边后的重心。
对于一条边 ((u,v)) ,删掉是否可以让 (x) 作为重心呢?
如果 (x) 不是重心,那么 ((u,v)) 必然在删掉 (x) 后的最大连通块内。考虑删掉 ((u,v)) 的意义就是去掉最大子树的一部分。设去掉的大小为 (S) ,一个节点的子树大小为 (s_x) ,最大子树为 (g_x) 。那么去掉 ((u,v)) 后可以让 (x) 成为重心必然满足:
- (displaystyle lfloor frac{n-S}{2} floorge g_x)
- (displaystyle lfloor frac{n-S}{2} floorge n-s_x-S)
那么可以解出来 (n-2s_xle Sle n-2g_x) 。而且不能去掉 (x) 的子树内的结点。
所以其实就是有一个 dfs 序的限制,一个子树大小的限制。直接离线+树状数组即可。
细节
细节主要在于换根。将结点 (u ightarrow v) 时会改变的只有 (u) 这一个点的重儿子(假设现在 (u) 是 (v) 的父亲,要向下换根)。具体是将 (u) 的儿子换为除了 (v) 以外的与 (u) 有边的节点中子树大小最大的。
代码
#include <cstdio>
using namespace std;
const int NN = 3e5 + 5;
int T, N, head[NN], totE, go[NN][21], up[NN];
int siz[NN], son[NN], secson[NN], fa[NN];
struct E {
int v, nxt;
E(int _v, int _nxt) : v(_v), nxt(_nxt) {}
E() : v(0), nxt(0) {}
} edge[NN * 2];
void AddE(int u, int v) {
edge[++totE] = E(v, head[u]);
head[u] = totE;
}
void calc(int u) {
for (int i = 1; i <= 18; ++i)
go[u][i] = go[go[u][i - 1]][i - 1];
}
void dfs1(int u, int f) {
siz[u] = 1;
fa[u] = f;
up[u] = f;
for (int p = head[u]; p; p = edge[p].nxt) {
int v = edge[p].v;
if (v != f) {
dfs1(v, u);
siz[u] += siz[v];
if (siz[son[u]] < siz[v]) {
secson[u] = son[u];
son[u] = v;
} else if (siz[v] > siz[secson[u]]) //这里是 else if
secson[u] = v;
}
}
go[u][0] = son[u];
calc(u);
}
long long ans;
void Find(int u) {
int now = u;
for (int i = 18; i >= 0; --i)
if (go[now][i] && (siz[u] - siz[go[now][i]]) * 2 <= siz[u])
now = go[now][i];
ans += now;
// printf("u %d
", u);
// printf("now %d ", now);
if (siz[now] * 2 == siz[u]) {
ans += fa[now];
// printf("fa %d", fa[now]);
}
// printf("
");
}
void dfs2(int u, int f) {
if (u != 1) {
siz[f] = siz[f] - siz[u] + siz[up[f]];
//注意 up 和 fa 的使用
int ffsz = siz[up[f]], hvson = up[f], oth = (son[f] == u) ? secson[f] : son[f];
if (ffsz < siz[oth]) hvson = oth;
go[f][0] = hvson;
calc(f);
fa[f] = u;
fa[u] = 0;
Find(u);
Find(f);
// printf("
");
}
for (int p = head[u]; p; p = edge[p].nxt) {
int v = edge[p].v;
if (v != f) {
dfs2(v, u);
}
}
siz[f] = siz[f] + siz[u] - siz[up[f]];
fa[u] = f;
fa[f] = 0;
go[f][0] = son[f];
calc(f);
}
int main() {
freopen("centroid.in", "r", stdin);
freopen("centroid.out", "w", stdout);
scanf("%d", &T);
while (T--) {
scanf("%d", &N);
for (int i = 1; i < N; ++i) {
int u, v;
scanf("%d%d", &u, &v);
AddE(u, v);
AddE(v, u);
}
dfs1(1, 0);
dfs2(1, 0);
printf("%lld
", ans);
ans = 0;
totE = 0;
for (int i = 0; i <= N; ++i) {
head[i] = son[i] = siz[i] = secson[i] = fa[i] = up[i] = 0;
for (int j = 0; j <= 18; ++j)
go[i][j] = 0;
}
}
fclose(stdin);
fclose(stdout);
return 0;
}
总结
以棵以 (u) 为根节点的树,重心要么是 (u) ,要么在 (u) 的重儿子里。