浅谈树形DP
树形DP
基本概念
核心思想:
-
树形DP的通性就是他维护的子结构一定是一个子树,即将树的结构作为DP的阶段。
-
每个节点的状态由其子节点的状态推导而来。
-
通常采用后序遍历的方式,即先处理子节点,后处理父节点。
基础模板
void dfs(int u, int fa) {
// 初始化当前节点的状态
// ...
for (const auto &v : e[u]) {
if (v == fa) continue;
dfs(v, u);
// 用子节点的状态更新当前节点的状态
// ...
}
}
例题
P1352 没有上司的舞会(最大独立集)
简单题,唯一坑点在需要自行找根,具体看代码实现:
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 6e3 + 10;
int n;
int r[N];
vector < int> e[N];
int dp[N][2];
int in[N];
int main() {
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> r[i];
for (int i = 1; i < n; ++i) {
int l, k; cin >> l >> k;
e[k].push_back(l);
++in[l];
}
auto dfs = [&] (int u, auto self) -> void {
dp[u][1] = r[u], dp[u][0] = 0;
for (const auto &v : e[u]) {
self(v, self);
dp[u][1] += dp[v][0];
dp[u][0] += max(dp[v][0], dp[v][1]);
}
};
int rt = -1;
for (int i = 1; i <= n; ++i)
if (!in[i]) rt = i;
// cout << rt << '\n';
dfs(rt, dfs);
cout << max(dp[rt][0], dp[rt][1]) << '\n';
return 0;
}
B4016 树的直径
核心思路:
对于树上的任意一个节点 \(u\) , 经过它的最长路径可以由它的两个子节点方向的最长路径拼接而成。
因此,我们只需要维护从 \(u\) 出发向下的最长距离和次长距离,二者之和就是经过 \(u\) 的最长路径。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
vector < int> e[N];
int n;
int main() {
cin >> n;
for (int i = 1; i < n; ++i) {
int u, v; cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
int ans = 0;
auto dfs = [&] (auto self, int u, int fa) -> int {
int d = 0, med = 0;
for (const auto &v : e[u]) {
if (v == fa) continue;
int dd = self(self, v, u) + 1;
if (dd > d) med = d, d = dd;
else if (dd > med) med = dd;
}
ans = max(ans, d + med);
return d;
};
dfs(dfs, 1, 0);
cout << ans << '\n';
return 0;
}
换根DP
换根DP是树形DP的一种特殊技巧。当我们需要以每个节点为根时的答案时,如果对每个节点都做一次 DFS ,时间复杂度是 \(O(n^2)\)。换根DP通过两次DFS在 \(O(n)\) 时间内解决。
基本思想:
-
先以任意点为根做一次 DFS ,统计各节点子树信息。
-
从根开始,通过父子关系转移,计算以其他点为根时的答案。
例题:P3478 [POI 2008] STA-Station
题意很明确了,找到一个根节点使树深度和最大化。
我们不妨设 \(dp[u]\) 表示以 \(u\) 为根节点时树的深度和。则当根从 \(u\) 转移到 \(v\) 时,不难发现的一点是,节点 \(v\) 及其子树对答案的贡献均减少了 \(1\),而其余节点对答案的贡献均增加了 \(1\)。
故我们可以列出转移式:
\(dp[v] = dp[u] - sz[v] + n - sz[v] = dp[u] + n - 2*sz[v]\)
代码很好实现,如下:
Code
#include <bits/stdc++.h>
#define endl '\n'
#define print(x, c) printf("%d%c", x, c)
#define inf INT_MAX
#define int long long
using namespace std;
const int N = 1e6 + 10;
inline int read() {
static int x, ch;
x = 0, ch = getchar();
while (ch < '0' || ch > '9') ch = getchar();
while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
return x;
}
#define rd(x) (x) = read()
int n, u, v, ans = 0, maxx = -inf;
int dep[N], siz[N], f[N];
vector < int> e[N];
signed main() {
// freopen("P3478.in", "r", stdin);
rd(n);
for (int i = 1; i <= (n - 1); ++i) {
rd(u), rd(v);
e[u].push_back(v);
e[v].push_back(u);
}
auto dfs1 = [&] (auto self, int u, int fa) -> void {
siz[u] = 1;
dep[u] = dep[fa] + 1;
for (const auto &v : e[u]) {
if (v == fa) continue;
self(self, v, u);
siz[u] += siz[v];
}
};
dfs1(dfs1, 1, 0);
for (int i = 1; i <= n; ++i) f[1] += dep[i];
auto dfs2 = [&] (auto self, int u, int fa) -> void {
for (const auto &v : e[u]) {
if (v == fa) continue;
f[v] = f[u] + n - 2 * siz[v];
self(self, v, u);
}
};
dfs2(dfs2, 1, 0);
for (int i = 1; i <= n; ++i)
if (f[i] > maxx)
maxx = f[i], ans = i;
return print(ans, endl), 0;
}
总结
-
当问题可以分解为树的子树问题,且子树的解可以合并为父节点的解时,适合用树形DP。
-
需要求出以每个节点为根时,适合使用换根DP。

浙公网安备 33010602011771号