【算法学习笔记】动态规划与数据结构的结合,在树上做DP
前置芝士:Here
本文是基于 OI wiki 上的文章加以修改完成,感谢社区的转载支持和其他方面的支持
树形 DP,即在树上进行的 DP。由于树固有的递归性质,树形 DP 一般都是递归进行的。
基础
以下面这道题为例,介绍一下树形 DP 的一般过程。
题目描述
某大学有
我们可以定义
显然,我们可以推出下面两个状态转移方程(其中下面的
(上司不参加舞会时,下属可以参加,也可以不参加) (上司参加舞会时,下属都不会参加)
我们可以通过 DFS,在返回上一层时更新当前结点的最优解。
代码
const int N = 1e4 + 10;
vector tr[N];
int f[N][2], v[N], Happy[N], n;
void dfs(int u) {
f[u][0] = 0; f[u][1] = Happy[u];
for (auto v : tr[u]) {
dfs(v);
f[u][0] += max(f[v][0], f[v][1]);
f[u][1] += f[v][0];
}
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
for (int i = 1; i <= n; ++i) cin >> Happy[i];
for (int i = 1, x, y; i < n; ++i) {
cin >> x >> y;
v[x] = 1;// x has a father
tr[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]) << "\n";
}
相关练习
树上背包
树上的背包问题,简单来说就是背包问题与树形 DP 的结合。
题目描述
现在有
一位学生要学习
每门课最多只有一门先修课的特点,与有根树中一个点最多只有一个父亲结点的特点类似。
因此可以想到根据这一性质建树,从而所有课程组成了一个森林的结构。为了方便起见,我们可以新增一门
我们设
转移的过程结合了树形 DP 和背包 DP 的特点,我们枚举
记点
注意上面转移方程中的几个限制条件,这些限制条件确保了一些无意义的状态不会被访问到。
我们可以证明,该做法的时间复杂度为
代码
const int N = 310;
vectore[N];
int f[N][N], s[N], n, m;
void dfs(int x) {
f[x][0] = 0;
for (int v : e[x]) { // 循环子节点(物品)
dfs(v);
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[v][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.tie(nullptr)->sync_with_stdio(false);
cin >> n >> m;
for (int i = 1, x; i <= n; ++i) {
cin >> x >> s[i];
e[x].push_back(i);
}
memset(f, 0xcf, sizeof(f)); // -inf
dfs(0);
cout << f[0][m] << "\n";
}
相关练习
换根 DP
树形 DP 中的换根 DP 问题又被称为二次扫描,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。
通常需要两次 DFS,第一次 DFS 预处理诸如深度,点权和之类的信息,在第二次 DFS 开始运行换根动态规划。
接下来以一些例题来带大家熟悉这个内容。
题目描述
给定一个
注意题目的样例给的输出是错误,正确的输出是
不妨令
考虑状态转移,这里就是体现"换根"的地方了。令
-
所有在
的子树上的结点深度都减少了一,那么总深度和就减少了 ; -
所有不在
的子树上的结点深度都增加了一,那么总深度和就增加了 ;
根据这两个条件就可以推出状态转移方程
于是在第二次 DFS 遍历整棵树并状态转移
代码
using pii = pair;
const int N = 2e5 + 10;
vectore[N];
int f[N], d[N], ff[N];
void dfs(int u, int fa) {
for (auto [v, w] : e[u]) {
if (v == fa) continue;
dfs(v, u);
if (d[v] == 1) f[u] += w;
else f[u] += min(f[v], w);
}
}
void dfs1(int u, int fa) {
ff[u] = f[u];
for (auto [v, w] : e[u]) {
if (v == fa)continue;
if (d[v] == 1) {
f[u] -= w;
f[v] += min(f[u], w);
} else {
f[u] -= min(w, f[v]);
f[v] += min(w, f[u]);
}
dfs1(v, u);
}
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int _; for (cin >> _; _--;) {
int n; cin >> n;
for (int i = 1; i <= n; ++i) {
e[i].clear();
d[i] = f[i] = ff[i] = 0;
}
for (int i = 1, u, v, w; i < n; ++i) {
cin >> u >> v >> w;
e[u].push_back({v, w});
e[v].push_back({u, w});
d[u]++, d[v]++;
}
dfs(1, -1);
dfs1(1, -1);
int ans = 0;
for (int i = 1; i <= n; ++i) ans = max(ans, ff[i]);
cout << ans << "\n";
}
}
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库
· 5. Nginx 负载均衡配置案例(附有详细截图说明++)
2020-08-06 算法学习笔记:连通图详解
2020-08-06 连通图算法详解之① :Tarjan 和 Kosaraju 算法