Day3
树的重心
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cstring>
#include<queue>
#define m_p make_pair
using namespace std;
using ll = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
using i128 = __int128;
typedef pair<int,int> pii;
const int maxn = 1e5 + 10;
int n;
//vector<vector<int> > g;
vector<int> g[maxn];
int sz[maxn];//size of
int min_balance = 0x3f3f3f3f,centroid = 0;
void dfs(int u,int fa){
int max_branch = 0;
sz[u] = 1;
for(auto& x : g[u]) {//son
if(x == fa) continue;
dfs(x,u);
sz[u] += sz[x];
max_branch = max(max_branch,sz[x]);
}
max_branch = max(max_branch,n - sz[u]);
if(max_branch < min_balance) {
centroid = u;
min_balance = max_branch;
}
}
void solve()
{
cin >> n;
for(int i = 1; i < n; i ++) {
int u,v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,0);
cout << centroid << " " << min_balance << "\n";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int t = 1;
while(t --) {
solve();
}
//system("pause");
return 0;
}
这是一个非常深刻的问题,触及到了递归(Recursion)和树形DP最核心的逻辑:“自底向上(Bottom-up)的信息传递”。
简单来说:因为爸爸不知道儿子有多大,必须等儿子量完自己的体重回来汇报,爸爸才能算出全家的总重量。
我们可以从以下三个角度来理解:
1. 数据的依赖关系 (Dependency)
我们看看 sz[u] 的定义公式:
\[sz[u] = 1 \text{ (自己)} + \sum sz[v] \text{ (所有孩子的子树大小)}
\]
在这个公式里,sz[u] 的值依赖于 sz[v] 的值。
- 在调用
dfs(v, u)之前,v只是一个没被访问过的节点,计算机根本不知道v下面挂了多少个节点,此时sz[v]可能还是初始值(比如 0)。 - 只有当
dfs(v, u)执行完毕(return回来) 之后,sz[v]才存储了正确的大小。 - 这时候,
u才能放心地执行sz[u] += sz[v]。
2. 形象的比喻:人口普查
想象你是一个村长 (u),你想统计全村有多少人。
你手下有几个组长 (v)。
-
错误的做法(先累加,再递归):
你直接问组长:“你组里多少人?”
组长还没去数呢,只能一脸懵逼地回答:“我还没去统计啊,现在的记录是 0。”
你记下 0,然后组长才出门去数人。
结果:你最后统计出来全村只有你自己 1 个人。 -
正确的做法(先递归,再累加):
你命令组长:“你先去(递归)把你那个组的人数数清楚,数完了回来告诉我。”
组长出门了,挨家挨户数完了,回来告诉你:“村长,我那组有 100 人。”
这时候(回溯),你把你本本上的数字加上 100。
结果:统计正确。
3. 代码对比
我们可以看看如果反过来写会发生什么:
❌ 错误的写法
void dfs(int u, int fa) {
sz[u] = 1;
for (int v : adj[u]) {
if (v == fa) continue;
// 此时 dfs(v) 还没跑,sz[v] 还是 0 !
sz[u] += sz[v]; // 加上了个寂寞
// 现在才去算 sz[v],晚了!u 已经算完了
dfs(v, u);
}
}
后果:整棵树算下来,所有节点的 sz 都是 1。
✅ 正确的写法 (回溯时更新)
void dfs(int u, int fa) {
sz[u] = 1;
for (int v : adj[u]) {
if (v == fa) continue;
// 1. 先派人下去把 v 及其子孙算清楚
dfs(v, u);
// 2. 既然 dfs(v) 跑完了,sz[v] 现在肯定有值了
// 把它加到我身上
sz[u] += sz[v];
}
}
总结
这就是“后序遍历(Post-order Traversal)”的思想:
先处理子节点,再处理根节点。
在树形 DP 中,凡是涉及“子树信息汇总”的操作(比如求子树大小、子树深度、子树权值和),都必须把计算逻辑写在递归调用之后。

浙公网安备 33010602011771号