总结一下最近练的树形dp
1.什么是树形dp
树形 DP,即在树上进行的 DP。由于树固有的递归性质,树形 DP 一般都是递归进行的。
大体上有两类树形dp:树上背包、换根 DP
树形动态规划是一种基于树形结构的动态规划算法。通常用于解决针对树形结构的优化问题,例如树上的最长路径、最大匹配等问题。
树形动态规划建立在树形结构上,通过对树形结构的遍历和状态转移来求解问题。在树形结构中,每个节点都有一些子节点和一个父节点,因此在树形动态规划中,通常需要考虑当前节点的状态以及它的子节点的状态,再通过状态转移方程来计算当前节点的最优状态。
树形动态规划的核心思想是将一个大的问题拆分成若干个小的子问题,并计算每个子问题的最优解,最后汇总得到整个问题的最优解。
2.需要准备的知识
树形,肯定需要去建立一个树,需要学会链式前向星等存图方法,还需要dfs/bfs等搜索方法
3.典型例题&板子
树上背包
给定一个含有 n 个节点的 树,以及树中每条边的 权值 .现需要在树中找出一条 路径,使得该路径上所有边的权值之和最大
我们考虑换一种 枚举方式:枚举路径的 起点和终点 → 枚举路径的 中间节点
那么经过他的路径有:
-以其 子树中的某个节点 作为 起点,以他作为 终点
-以其 子树中的某个节点 作为 起点,以 子树中的某个节点 作为 终点
-以其 子树中的某个节点 作为 起点,以 非其子树的节点 作为 终点
对于第 1 种情况,我们可以直接递归处理其子树,找出到当前子树根节点最长的路径长度即可
对于第 2 种情况,我们在处理第 1 种情况时,顺便找出 1 类路径的 次长路径, 最长 和 次长 拼在一起,就是我们要的第 2 种情况
而对于第 3 种情况,我们可以把它归类为其 祖先节点 的第 1,2 种情况,让其 祖先节点 去处理即可
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e4 + 10, M = N << 1; //初始不确定树的拓扑结构,因此要建立双向边
int n;
int h[N], e[M], w[M], ne[M], idx;
int f1[N], f2[N], res;
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u, int father)
{
f1[u] = f2[u] = 0;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == father) continue;
dfs(j, u);
if (f1[j] + w[i] >= f1[u]) f2[u] = f1[u] ,f1[u] = f1[j] + w[i]; //最长路转移
else if (f1[j] + w[i] > f2[u]) f2[u] = f1[j] + w[i]; //次长路转移
}
res = max(res, f1[u] + f2[u]);
}
int main()
{
memset(h, -1, sizeof h);
scanf("%d", &n);
for (int i = 0; i < n - 1; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
dfs(1, -1); //我们可以任意选取一个点作为根节点,这样整棵树的拓扑结构被唯一确定下来了
printf("%d\n", res);
return 0;
}
换根dp
换根DP核心思想就是两次dfs,先计算子节点的贡献,后计算父节点的贡献
换根DP 一般分为三个步骤:
1 指定任意一个根节点
2 一次dfs遍历,统计出当前子树内的节点对当前节点的贡献
3 一次dfs遍历,统计出当前节点的父节点对当前节点的贡献,然后合并统计答案
那么我们就要先 dfs 一遍,预处理出当前子树对于根的最大贡献(距离)和 次大贡献(距离)
处理 次大贡献(距离) 的原因是:
如果 当前节点 是其 父节点子树 的 最大路径 上的点,则 父节点子树 的 最大贡献 不能算作对该节点的贡献
因为我们的路径是 简单路径,不能 走回头路
然后我们再 dfs 一遍,求解出每个节点的父节点对他的贡献(即每个节点往上能到的最远路径
两者比较,取一个 max 即可
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010, M = N << 1, INF = 0x3f3f3f3f;
int n;
int h[N], e[M], w[M], ne[M], idx;
int d1[N], d2[N], up[N];
int s1[N], s2[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs1(int u, int father)
{
// d1[u] = d2[u] = -INF; //这题所有边权都是正的,可以不用初始化为负无穷
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == father) continue;
dfs1(j, u);
if (d1[j] + w[i] >= d1[u])
{
d2[u] = d1[u], s2[u] = s1[u];
d1[u] = d1[j] + w[i], s1[u] = j;
}
else if (d1[j] + w[i] > d2[u])
{
d2[u] = d1[j] + w[i], s2[u] = j;
}
}
// if (d1[u] == -INF) d1[u] = d2[u] = 0; //特判叶子结点
}
void dfs2(int u, int father)
{
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == father) continue;
if (s1[u] == j) up[j] = w[i] + max(up[u], d2[u]); //son_u = j,则用次大更新
else up[j] = w[i] + max(up[u], d1[u]); //son_u != j,则用最大更新
dfs2(j, u);
}
}
int main()
{
memset(h, -1, sizeof h);
scanf("%d", &n);
for (int i = 1; i < n; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
dfs1(1, -1);
dfs2(1, -1);
int res = INF;
for (int i = 1; i <= n; i ++ ) res = min(res, max(d1[i], up[i]));
printf("%d\n", res);
return 0;
}
注:
链式前向星存图:
const int N = 10010, M = N << 1;
int n;
int h[N], e[M], w[M], ne[M], idx;
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
树形dfs:
void dfs(int u, int father)
{
// d1[u] = d2[u] = -INF; //这题所有边权都是正的,可以不用初始化为负无穷
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == father) continue;
dfs(j, u);
//下面写dp
}
// if (d1[u] == -INF) d1[u] = d2[u] = 0; //特判叶子结点
}
无向图开数组要开两倍
dfs(1,-1); 搜索