洛谷P3478是一道经典的树形动态规划问题,出自波兰信息学竞赛POI 2008。题目要求我们在一棵无根树中找到一个节点,使得当该节点作为根时,树中所有节点的深度之和最大。这类问题在算法竞赛中被称为"换根DP"问题,是树形DP的重要变种。
给定一棵包含n个节点的树(无向无环连通图),我们需要找到一个节点u,使得以u为根时,所有节点深度之和最大。节点的深度定义为该节点到根节点的路径上的边数。
输入格式:
输出格式:
最直观的解法是对每个节点作为根的情况分别计算深度和,然后取最大值。对于每个根节点,我们可以通过一次DFS或BFS遍历计算所有节点的深度,时间复杂度为O(n)。由于需要对n个节点都做一次这样的计算,总时间复杂度为O(n^2)。
对于n=10^6的数据规模,O(n^2)的算法显然无法在合理时间内完成(需要约10^12次操作,现代计算机需要数小时)。因此,我们需要更高效的算法,这就是换根DP的价值所在。
换根DP是一种优化树形DP的技术,其核心思想是通过两次遍历(通常是DFS)来高效计算以每个节点为根时的某些树属性,而无需对每个根节点都重新遍历整棵树。
第一次DFS(后序遍历):选择一个初始根节点(通常选1号节点),计算以该节点为根时的子树信息,并计算初始的深度和。
第二次DFS(前序遍历):通过父节点的信息推导子节点的信息,实现"换根"操作。当我们把根从父节点u转移到子节点v时,可以高效地更新深度和。
设当前根为u,考虑将根转移到其子节点v时,深度和的变化:
设size[v]表示以u为根时v的子树大小,则深度和的变化量为:
delta = (n - size[v]) - size[v] = n - 2 * size[v]
因此,新的深度和:
sum[v] = sum[u] + (n - 2 * size[v])
这个公式是换根DP的核心,它允许我们在O(1)时间内计算相邻节点的深度和。
首先我们需要建立树的邻接表表示。由于n可能很大(10^6),我们通常使用vector数组来存储:
cpp复制vector<int> tree[MAXN]; // MAXN = 1e6 + 10
cpp复制int size[MAXN]; // 子树大小
long long sum[MAXN]; // 深度和
void dfs1(int u, int parent, int depth) {
size[u] = 1;
sum[0] += depth; // 假设初始根为0,累加所有深度
for (int v : tree[u]) {
if (v == parent) continue;
dfs1(v, u, depth + 1);
size[u] += size[v];
}
}
cpp复制void dfs2(int u, int parent) {
for (int v : tree[u]) {
if (v == parent) continue;
sum[v] = sum[u] + (n - 2 * size[v]);
dfs2(v, u);
}
}
这种线性复杂度对于n=10^6的数据规模是完全可行的,可以在现代计算机上快速完成。
对于n=10^6的树,如果树退化成链,递归深度会达到10^6,可能导致栈溢出。解决方法:
深度和可能达到n^2级别(对于n=10^6,最大约5e11),因此需要使用64位整数(long long)存储。
对于n很大的情况,可以使用更紧凑的数据结构:
cpp复制vector<vector<int>> tree(n + 1); // 动态分配,节省空间
对于大规模数据,使用快速的IO方法:
cpp复制ios::sync_with_stdio(false);
cin.tie(nullptr);
cpp复制#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 1e6 + 10;
vector<int> tree[MAXN];
int size[MAXN];
long long sum[MAXN];
int n;
void dfs1(int u, int parent, int depth) {
size[u] = 1;
sum[0] += depth;
for (int v : tree[u]) {
if (v == parent) continue;
dfs1(v, u, depth + 1);
size[u] += size[v];
}
}
void dfs2(int u, int parent) {
for (int v : tree[u]) {
if (v == parent) continue;
sum[v] = sum[u] + (n - 2 * size[v]);
dfs2(v, u);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i < n; ++i) {
int a, b;
cin >> a >> b;
tree[a].push_back(b);
tree[b].push_back(a);
}
dfs1(1, -1, 0);
dfs2(1, -1);
int ans = 1;
for (int i = 2; i <= n; ++i) {
if (sum[i] > sum[ans]) {
ans = i;
}
}
cout << ans << endl;
return 0;
}
换根DP技术不仅适用于本题,还可以解决许多其他树形问题:
理解换根DP的关键在于把握两点:
这种思想在解决树形结构问题时非常强大,可以显著提高算法效率。