树形 DP
树形 DP,即在树上进行的 DP。由于树固有的递归性质,树形 DP 一般都是递归进行的。
基础
以下面这道题为例,介绍一下树形 DP 的一般过程。
某大学有
我们设
对于每个状态,都存在两种决策(其中下面的
- 上司不参加舞会时,下属可以参加,也可以不参加,此时有
; - 上司参加舞会时,下属都不会参加,此时有
。
我们可以通过 DFS,在返回上一层时更新当前结点的最优解。
#include <algorithm>
#include <cstdio>
using namespace std;
struct edge {
int v, next;
} e[6005];
int head[6005], n, cnt, f[6005][2], ans, is_h[6005], vis[6005];
void addedge(int u, int v) { // 建图
e[++cnt].v = v;
e[cnt].next = head[u];
head[u] = cnt;
}
void calc(int k) {
vis[k] = 1;
for (int i = head[k]; i; i = e[i].next) { // 枚举该结点的每个子结点
if (vis[e[i].v]) continue;
calc(e[i].v);
f[k][1] += f[e[i].v][0];
f[k][0] += max(f[e[i].v][0], f[e[i].v][1]); // 转移方程
}
return;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &f[i][1]);
for (int i = 1; i < n; i++) {
int l, k;
scanf("%d%d", &l, &k);
is_h[l] = 1;
addedge(k, l);
}
for (int i = 1; i <= n; i++)
if (!is_h[i]) { // 从根结点开始DFS
calc(i);
printf("%d", max(f[i][1], f[i][0]));
return 0;
}
}
习题
树上背包
树上的背包问题,简单来说就是背包问题与树形 DP 的结合。
现在有
一位学生要学习
每门课最多只有一门先修课的特点,与有根树中一个点最多只有一个父亲结点的特点类似。
因此可以想到根据这一性质建树,从而所有课程组成了一个森林的结构。为了方便起见,我们可以新增一门
我们设
转移的过程结合了树形 DP 和 背包 DP 的特点,我们枚举
记点
注意上面状态转移方程中的几个限制条件,这些限制条件确保了一些无意义的状态不会被访问到。
可以证明,该做法的时间复杂度为
#include <algorithm>
#include <cstdio>
#include <vector>
using namespace std;
int f[305][305], s[305], n, m;
vector<int> e[305];
int dfs(int u) {
int p = 1;
f[u][1] = s[u];
for (auto v : e[u]) {
int siz = dfs(v);
// 注意下面两重循环的上界和下界
// 只考虑已经合并过的子树,以及选的课程数超过 m+1 的状态没有意义
for (int i = min(p, m + 1); i; i--)
for (int j = 1; j <= siz && i + j <= m + 1; j++)
f[u][i + j] = max(f[u][i + j], f[u][i] + f[v][j]); // 转移方程
p += siz;
}
return p;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
int k;
scanf("%d%d", &k, &s[i]);
e[k].push_back(i);
}
dfs(0);
printf("%d", f[0][m + 1]);
return 0;
}
习题
换根 DP
树形 DP 中的换根 DP 问题又被称为二次扫描,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。
通常需要两次 DFS,第一次 DFS 预处理诸如深度,点权和之类的信息,在第二次 DFS 开始运行换根动态规划。
接下来以一些例题来带大家熟悉这个内容。
给定一个
不妨令
考虑状态转移,这里就是体现"换根"的地方了。令
-
所有在
的子树上的结点深度都减少了一,那么总深度和就减少了 ; -
所有不在
的子树上的结点深度都增加了一,那么总深度和就增加了 ;
根据这两个条件就可以推出状态转移方程
于是在第二次 DFS 遍历整棵树并状态转移
#include <bits/stdc++.h>
using namespace std;
int head[1000010 << 1], tot;
long long n, sz[1000010], dep[1000010];
long long f[1000010];
struct node {
int to, next;
} e[1000010 << 1];
void add(int u, int v) { // 建图
e[++tot] = {v, head[u]};
head[u] = tot;
}
void dfs(int u, int fa) { // 预处理dfs
sz[u] = 1;
dep[u] = dep[fa] + 1;
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (v != fa) {
dfs(v, u);
sz[u] += sz[v];
}
}
}
void get_ans(int u, int fa) { // 第二次dfs换根dp
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (v != fa) {
f[v] = f[u] - sz[v] * 2 + n;
get_ans(v, u);
}
}
}
int main() {
scanf("%lld", &n);
int u, v;
for (int i = 1; i <= n - 1; i++) {
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
dfs(1, 1);
for (int i = 1; i <= n; i++) f[1] += dep[i];
get_ans(1, 1);
long long int ans = -1;
int id;
for (int i = 1; i <= n; i++) { // 统计答案
if (f[i] > ans) {
ans = f[i];
id = i;
}
}
printf("%d\n", id);
return 0;
}
习题
参考资料与注释
Footnotes
贡献者:@ricofx47@Shuhao@H-J-Granger@kenlig@rui_er@thredreams@Danni@mgt@Ravenclaw-OIer@Early@邵晨恒@sshwy@Henry-ZHR@ouuan@Billchenchina@心旷神怡@Ir1d
本页面最近更新:2/3/2023, 12:00:00 AM,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用