换根dp
拿一道比较经典的例题来作为引入
[POI2008] STA-Station
给定一个 \(n\) 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和(路径和)最大。
一个结点的深度之定义为该节点到根的简单路径上边的数量。
\(1 \leq n \leq 10^6\),\(1 \leq u, v \leq n\),给出的是一棵树。
首先如果是有根树的话就是一个比较简单的树形dp了,令 \(f_x\) 代表 \(x\) 的子树中所有的路径和,\(size[x]\) 为 \(x\) 的子树中的点的数量,那么 \(f_x=\sum_{y\Subset son_x}f_y+size[y]\)。那么可以得到一个 \(O(n^2)\) 的做法,不足以通过此题。
可以注意到我们每次枚举的过程其实就是相当于换根的过程,之所以复杂度不对是因为在换根的过程中很多部分都被重复计算了,例如当我的根从 \(x\) 换到 \(y\) 的时候,以前以 \(x\) 为根和现在以 \(y\) 为根的 \(y\) 的子树对 \(f_y\) 的贡献是不变的,只不过现在 \(x\) 加到了 \(y\) 的子树当中,我们只需要求出这一部分的贡献就可以了。
我们令 \(v_x\) 为把 \(x\) 的父亲变成 \(x\) 的子树所带来的贡献,那么当我的根从 \(1\) 换到 \(x\) 的时候,我们易得 \(v_x=f_1-f_x-size[x]+n-size[x]\)。更广泛一点,对于任意一对点 \(x,y\),其中 \(x\) 是 \(y\) 的父亲,\(v_y=f_x+v_x-size[y]+n-size[y]\),其中 \(f_x+v[x]\) 就是以 \(x\) 为根的时候的子树中的路径和。
AC code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
ios_base::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
int n; cin >> n;
vector<vector<int>> g(n + 1);
for (int i = 0; i < n - 1; i++) {
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
vector<ll> f(n + 1), v(n + 1);
vector<int> size(n + 1);
auto up = [&](auto up, int x, int pre) -> void {
size[x] = 1;
for (int y : g[x]) if (y != pre) {
up(up, y, x);
sz[x] += sz[y];
f[x] += f[y] + sz[y];
}
};
up(up, 1, 0);
auto down = [&](auto down, int x, int pre) -> void {
for (int y : g[x]) if (y != pre) {
w[y] = w[x] + f[x] - f[y] - sz[y] + n - sz[y];
down(down, y, x);
}
};
down(down, 1, 0);
ll ans = 0, r = -1;
for (int i = 1; i <= n; i++) if (setmax(ans, f[i] + v[i])) r = i;
cout << r << '\n';
return 0;
}
总结一下,一般的换根dp都有一下套路:
- 首先假设 \(1\) 为根进行一次从下往上树形dp (up)
- 分析 \(x\) 与 \(1\) 进行换根
- 泛化到任意一对 \((x,y)\) 进行从上往下换根 (down)
problem set:

浙公网安备 33010602011771号