# 0x54 动态规划-树形DP
A.没有上司的舞会 基础树形DP
emmm,蒟蒻发现自己的DP太辣鸡了。。。所以来练练DP,这题的话实际上应该算是树DP的入门题吧,转移还是挺好想的。
每次在每个节点都会有个选择,就是选还是不选,如果选的话,那么它的儿子节点就不能选,如果不选的话它的儿子节点就可以选,也就是说我们需要另开一维状态来记录每个节点是否选自己的情况,那么就很容易得出如下方程:
dp[x][0]+=max(0,max(dp[v][1],dp[v][0]));//如果不选当前节点,那么儿子节点可以任意选
dp[x][1]+=max(0,dp[v][0]);//如果选择当前节点,那么只能选择儿子节点不存在的情况
AC代码
#include <bits/stdc++.h>
using namespace std;
vector<int> son[10010];
int f[10010][2], v[10010], happy[10010], n;
void dfs(int x) {
f[x][0] = 0, f[x][1] = happy[x];
for (int i = 0; i < son[x].size(); ++i) {
int y = son[x][i];
dfs(y);
f[x][0] += max(f[y][0], f[y][1]);
f[x][1] += f[y][0];
}
}
int main() {
//freopen("in.txt", "r", stdin);
cin >> n;
for (int i = 1; i <= n; ++i) cin >> happy[i]; //快乐指数
for (int i = 1; i < n; ++i) {
int x, y;
cin >> x >> y;
v[x] = 1; // x has a father
son[y].push_back(x);
}
int root;
for (int i = 1; i <= n; ++i)
if (!v[i]) {
root = i;
break;
}
dfs(root);
cout << max(f[root][0], f[root][1]) << endl;
}
算法竞赛进阶指南原文:
正如深度优先和广度优先都可以对树或图进行遍历一样,除了自顶向下的递归外。我们也可以使用自底向上的Topo排序来执行树形DP。但通常前者就足够了。
B.选课 背包类树形DP
思路:
每堂课和学它必修的课连一条边。为了方便,每个入度为0的课(即可以直接选的课)与一个虚拟的n+1节点连一条边,然后在树上跑01背包即可。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
vector<int> son[310];
int f[310][310], s[310], n, m;
void dp(int x) {
f[x][0] = 0;
for (int i = 0; i < son[x].size(); i++) { // 循环子节点(物品)
int y = son[x][i];
dp(y);
for (int t = m; t >= 0; t--) // 倒序循环当前选课总门数(当前背包体积)
for (int j = 0; j <= t; j++) // 循环更深子树上的选课门数(组内物品)
f[x][t] = max(f[x][t], f[x][t - j] + f[y][j]);
/* 或者
for (int j = t; j >= 0; j--)
if (t + j <= m)
f[x][t+j] = max(f[x][t+j], f[x][t] + f[y][j]);
这两种写法j分别用了正序和倒序循环
是为了正确处理组内体积为0的物品(本题正序倒序都可以AC是因为体积为0的物品价值恰好也为0)
请读者结合0/1背包问题中DP的“阶段”理论思考 */
}
if (x != 0) // x不为0时,选修x本身需要占用1门课,并获得相应学分
for (int t = m; t > 0; t--)
f[x][t] = f[x][t - 1] + s[x];
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int x;
cin >> x >> s[i];
son[x].push_back(i);
}
memset(f, 0xcf, sizeof(f)); // -INF
dp(0);
cout << f[0][m] << endl;
}
C.Accumulation Degree
一个树形水系,有 \(n\) 个结点,根结点称为源点,叶子结点称为汇点,每条边都有水量限制$C(x,y) \((\)x,y$ 为这条边的两个端点),源点单位时间流出的水量称为整个水系的流量,求以哪一个结点作为源点整个水系的流量最大。
首先得理解到这是一道“不定根”的树形DP问题,这类题目的特点是,给定一个树形结构,需要以每个结点为根进行一系列统计。我们一般通过两次扫描来求解此类问题:(也即:二次扫描与换根法)
- 第一次扫描时任选一个点为根,在“有根树”上执行一次“树形DP”,也就是在回溯时发生的、自底向上的状态转移。
- 第二次扫描,从刚才选出的根出发,对整棵树执行一次DFS,在每次递归前进行自上而下的推导,计算出“换根”之后的解。
首先,我们任选一个结点 root
,然后树形DP一下,求出 \(D_{root}\) 数组( \(D[i]\) 表示在以 \(i\) 为根的子树中流量的最大值)。然后设 \(f_x\) 表示以 \(x\) 为源点,流向整个水系的最大流量,则显然 \(f_{root} = D_{root}\) 假设 \(f_x\) 已经求出,考虑其子结点 \(y\) ,则 \(f[y]\) 包含两部分:
- 从 \(y\) 流向以 \(y\) 为根的子树的流量,已经计算出来。
- 从 \(y\) 沿着到父节点 \(x\) 的河道,进而流向水系中其他部分的流量。
由题意可知,从 \(x\) 流向 \(y\) 的流量为 \(min(D_y,c_{x,y})\) ,所以从 \(x\) 流向除 \(y\) 以外其他部分的流量分量是其两者之差:\(f_x - min(D_y,c_{x,y})\) 于是,把 \(y\) 作为源点,先从流到 \(x\),再流向其他部分的流量就是吧这个“差值”再与 \(c_{x,y}\) 取较小值后的结果
这是一个由下而上的递推方程,所以我们可以通过一次DFS来完成
AC 代码
// Murabito-B 21/04/26
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
const int N = 2e5 + 5;
using pii = pair<int, int>;
vector<pii> g[N];
int dp[N], d[N], f[N];
void dfs(int u, int fa) {
for (int i = 0; i < g[u].size(); ++i) {
int v = g[u][i].fi, w = g[u][i].se;
if (v == fa) continue;
dfs(v, u);
if (d[v] == 1) dp[u] += w;
else
dp[u] += min(dp[v], w);
}
}
void dfs1(int u, int fa) {
f[u] = dp[u];
for (int i = 0; i < g[u].size(); ++i) {
int v = g[u][i].fi, w = g[u][i].se;
if (v == fa) continue;
if (d[v] == 1) {
dp[u] -= w;
dp[v] += min(dp[u], w);
} else {
dp[u] -= min(w, dp[v]);
dp[v] += min(w, dp[u]);
}
dfs1(v, u);
}
}
void solve() {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) {
g[i].clear();
d[i] = dp[i] = f[i] = 0;
}
for (int i = 1; i < n; ++i) {
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({v, w});
g[v].push_back({u, w});
d[u]++, d[v]++;
}
dfs(1, -1);
// f[1] = dp[1];
dfs1(1, -1);
int ans = 0;
for (int i = 1; i <= n; ++i) ans = max(ans, f[i]);
cout << ans << '\n';
}
int main() {
ios_base::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int _;
for (cin >> _; _--;) solve();
return 0;
}