洛谷P10962是一道典型的树形动态规划问题,需要运用换根DP技术解决。题目给出一个n个节点的树结构,要求计算每个节点作为根节点时的特定属性值。这类问题在算法竞赛中具有重要地位,特别是在处理树形结构的动态规划问题时。
题目要求我们对树中的每个节点u,计算当u作为整棵树的根时,所有节点到u的距离之和。直接暴力解法是对每个节点进行一次DFS/BFS遍历,时间复杂度为O(n²),这在n较大时(如n=1e5)会超时。换根DP技术可以将时间复杂度优化到O(n)。
换根DP是一种分两阶段处理树形DP的技术:
这种技术的精妙之处在于利用了树结构的特殊性质 - 当根节点从父节点u转移到子节点v时,只有u和v之间的连接关系发生变化,其他节点的状态可以复用。
我们需要维护以下几个关键数组:
size[u]:以u为根的子树大小(节点数量)dp[u]:以u为根时,所有节点到u的距离之和parent[u]:u的父节点(用于遍历树结构)cpp复制const int N = 1e5 + 10;
vector<int> g[N]; // 邻接表存储树
int size[N], dp[N], parent[N];
从任意根节点(通常选1)开始后序遍历,计算子树大小和初始dp值:
cpp复制void dfs1(int u, int fa) {
size[u] = 1;
dp[u] = 0;
parent[u] = fa;
for (int v : g[u]) {
if (v == fa) continue;
dfs1(v, u);
size[u] += size[v];
dp[u] += dp[v] + size[v]; // 每增加一条边,距离+1
}
}
关键点说明:
dp[v]表示以v为根的子树内部节点到v的距离和size[v]表示子树v中的节点数核心转移方程:
当根从u转移到其子节点v时:
因此状态转移方程为:
dp[v] = dp[u] - size[v] + (n - size[v])
实现代码:
cpp复制void dfs2(int u, int fa) {
for (int v : g[u]) {
if (v == fa) continue;
dp[v] = dp[u] - size[v] + (g.size() - size[v]);
dfs2(v, u);
}
}
cpp复制#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
vector<int> g[N];
int size[N], dp[N], n;
void dfs1(int u, int fa) {
size[u] = 1;
dp[u] = 0;
for (int v : g[u]) {
if (v == fa) continue;
dfs1(v, u);
size[u] += size[v];
dp[u] += dp[v] + size[v];
}
}
void dfs2(int u, int fa) {
for (int v : g[u]) {
if (v == fa) continue;
dp[v] = dp[u] - size[v] + (n - size[v]);
dfs2(v, u);
}
}
int main() {
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);
}
dfs1(1, 0);
dfs2(1, 0);
for (int i = 1; i <= n; ++i) {
cout << dp[i] << "\n";
}
return 0;
}
数组越界问题:
转移方程错误:
重复计算问题:
小样例测试法:
构造3-5个节点的小树,手工计算每个节点的dp值,与程序输出对比
中间输出调试:
在dfs1和dfs2的关键位置输出size和dp数组,观察变化过程
边界条件检查:
特别测试n=1和n=2的极端情况,确保程序正确处理
带权树的情况:
将size[v]替换为子树v的权重和,距离计算考虑边权
多属性维护:
除了距离和,还可以维护最大值、最小值等其他属性
有向树处理:
需要调整转移方程,考虑边的方向性
在实际比赛中,换根DP是解决树形结构问题的利器。掌握这个技术后,可以高效处理许多看似复杂的树形问题。我个人的经验是,先手工推导小样例,确保完全理解状态转移的逻辑,再着手编写代码,这样能大大减少调试时间。