以 1 为根求一次深度和与子树大小,再沿父子边换根:D(v) = D(u) + n − 2·sz(v),两遍 O(n) 扫完整棵树,取深度和最大的点。
OJ: luogu
题目 ID: P3478
难度:普及+/提高-
形式化题目
给定一棵
把以
要求输出一个使
暴力解法
思路
最直白的做法是逐个点试一遍:把
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:每个点都 BFS 计算距离和,只适合小数据。
const int MAXN = 505;
int n;
vector<int> graph_edges[MAXN];
long long distance_sum_from(int start) {
int dist[MAXN];
for (int i = 1; i <= n; i++) {
dist[i] = -1;
}
queue<int> que;
que.push(start);
dist[start] = 0;
while (!que.empty()) {
int u = que.front();
que.pop();
for (int i = 0; i < (int)graph_edges[u].size(); i++) {
int v = graph_edges[u][i];
if (dist[v] == -1) {
dist[v] = dist[u] + 1;
que.push(v);
}
}
}
long long sum = 0;
for (int i = 1; i <= n; i++) {
sum += dist[i];
}
return sum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
graph_edges[u].push_back(v);
graph_edges[v].push_back(u);
}
int best_node = 1;
long long best_value = distance_sum_from(1);
for (int i = 2; i <= n; i++) {
long long value = distance_sum_from(i);
if (value > best_value) {
best_value = value;
best_node = i;
}
}
cout << best_node << '\n';
return 0;
}复杂度
单个点 BFS 一遍是
瓶颈
瓶颈不在单次遍历,而在于「换一个根就重头走一遍」:
正解:换根 DP
思路
先随便挑一个点当根,这里取
:以 为根时 的子树点数; :以 为根时的深度和,也就是题目要最大化的那个量。
第一步,算出根
第二步,看父子之间怎么挪。 设
拿样例核对这张图。
子树里的 个点,到新根不用再绕经边 ,各少走一步,深度和减少 ; - 其余
个点,到新根必须多跨这条边,各多走一步,深度和增加 。
两边相抵就得到换根公式:
样例里
第三步,把整棵树推一遍。 从根
顺便记下这个式子的含义:
代码
代码里 subtree_size 就是 dist_sum 就是 parent_node 记录以
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
int n;
int head[MAXN], to[MAXN * 2], nxt[MAXN * 2], edge_cnt;
int parent_node[MAXN];
int subtree_size[MAXN];
int depth_node[MAXN];
long long dist_sum[MAXN]; // dist_sum[u] 表示以 u 为根时,所有点深度之和。
vector<int> order_nodes;
void add_edge(int u, int v) {
edge_cnt++;
to[edge_cnt] = v;
nxt[edge_cnt] = head[u];
head[u] = edge_cnt;
}
void read_input() {
cin >> n;
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
add_edge(u, v);
add_edge(v, u);
}
}
void build_order() {
order_nodes.reserve(n);
queue<int> que;
que.push(1);
parent_node[1] = 0;
depth_node[1] = 0;
while (!que.empty()) {
int u = que.front();
que.pop();
order_nodes.push_back(u);
for (int i = head[u]; i != 0; i = nxt[i]) {
int v = to[i];
if (v == parent_node[u]) {
continue;
}
parent_node[v] = u;
depth_node[v] = depth_node[u] + 1;
que.push(v);
}
}
}
void solve() {
build_order();
long long root_sum = 0;
for (int i = 0; i < (int)order_nodes.size(); i++) {
int u = order_nodes[i];
subtree_size[u] = 1;
root_sum += depth_node[u];
}
dist_sum[1] = root_sum;
for (int i = (int)order_nodes.size() - 1; i >= 0; i--) {
int u = order_nodes[i];
if (parent_node[u] != 0) {
subtree_size[parent_node[u]] += subtree_size[u];
}
}
for (int i = 0; i < (int)order_nodes.size(); i++) {
int u = order_nodes[i];
for (int e = head[u]; e != 0; e = nxt[e]) {
int v = to[e];
if (v == parent_node[u]) {
continue;
}
// 根从 u 换到孩子 v:
// v 子树内的点深度都 -1,其余点深度都 +1。
dist_sum[v] = dist_sum[u] - subtree_size[v] + (n - subtree_size[v]);
}
}
int best_node = 1;
for (int i = 2; i <= n; i++) {
if (dist_sum[i] > dist_sum[best_node]) {
best_node = i;
}
}
cout << best_node << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}实现上有两处是冲着
- 不用递归。链形树的深度能到
,递归 DFS 直接爆栈。代码先用一次 BFS 生成遍历顺序 order_nodes,正序算深度和,逆序累加子树大小,再正序换根,全程只用一个队列和几个数组。 - 深度和用
long long。链形树上的量级是 , int装不下。
复杂度
- 时间:建图
,BFS 求深度与顺序 ,逆序统计子树大小 ,换根递推 ,扫描取最大 ,总 。 - 空间:链式前向星
,加上父亲、深度、子树大小、答案四个数组,共 。实测 的链数据峰值内存约 43 MiB。
总结
这题的转折点是承认「相邻两个根的答案只差一笔账」,然后用子树大小把这笔账
一条边的两侧点数决定了深度和往哪个方向变,于是
两处容易踩的地方:
- 别把「答案是叶子」推成「答案是直径端点」。 有人看到
最大就以为根必须挂在直径的某一端,这不成立。反例是 个点的树,边为 、 、 、 、 、 、 、 、 、 、 、 :最大值点在 , ,可 到最远点的距离只有 ,而直径是 (端点有 )。 是叶子,但不在直径端点上。真要拿性质剪枝,能用的只有「答案一定是叶子」这一条。 - 规模上去了,实现方式就得跟着换。 本题
比 P1395 大一个量级,递归写法在链数据上会 RE,必须改成 BFS 定序加逆序统计。
如果想继续练换根,可以把