\(\large\texttt{Warning:}\) 此篇博客中的代码是本人在 \(2019\) 年到 \(2021\) 年间断断续续写的,所以码风有较大的差异 ,后期会更改代码。
树形 \(DP\)
只要你学会了树,还学会了 \(dp\) ,那么你就学会了树形 \(dp\) 。\(By\) 某不愿透露姓名的教练
-
什么是树形 \(DP\)
树形 \(DP\),就是在“树”的数据结构上的动态规划,一般状态转移都是和子树相关,且能与线段树等数据结构相结合。
因为其具有传递性,就对于具有一定规律的树上问题求解起到了很大帮助。
-
常见的题型
-
子树和计数:
这类问题主要是统计子树和,通过加减一些子树满足题目中要求的某些性质
例如 \(CF767C、Luogu\ P1122\)
-
树上背包问题:
这类问题就是让你求在树上选一些点满足价值最大的问题,一般都可以设 \(\large f_{i,j}\) 表示 \(i\) 这颗子树选 \(j\) 个点的最优解。
例如 \(Luogu\ P1272\ P1273\)
-
花费最少的费用覆盖所有点:
这类问题是父亲与孩子有联系的题。基本有两种类型:
-
选父亲必须不能选孩子(强制)
-
选父亲可以不用选孩子(不强制)
例如 \(UVA\ 1220\)(类型1)、\(Luogu\ P2458\)(类型2)
-
-
树上统计方案数:
这类问题就是给你一个条件,问你有多少个点的集合满足这样的条件。这类题主要运用乘法原理,控制一个点不动,看他能做多少贡献
-
与多种算法结合:
这类问题就只能根据题目分析,听天由命了\(\cdots\cdots\)
-
-
例题:
-
\(Luogu\ P2015\) 二叉苹果树:
这道题属于常见题型中的树上背包问题,可以将其作为模板题。
这道题还有一个隐含的条件,当某条边被保留下来时,从根节点到这条边的路径上的所有边也都必须保留下来。
所以,我们可以很容易定义我们的 \(dp\) 状态。令 \(\large f_{i,j}\) 表示在 \(i\) 子树中保留 \(j\) 条边能够得到的最大苹果树。
那么,状态转移方程就显而易见了:\(\large\mathcal{ f_{u,i}=\max(f_{u,i},f_{u,i-j-1}+f_{v,j}+Apple_{u,v})}\) ,其中 \(v\) 为 \(u\) 的子节点,\(\mathcal{Apple_{u,v}}\) 表示 \(u \rightarrow v\) 这条边上的苹果数。
注意: 由于这是一个 \(0/1\) 背包,所以 \(i、j\) 需要倒序遍历。
代码:
#include <cstdio> #include <vector> #include <algorithm> using namespace std; const int MAXN = 110; typedef pair<int, int> T; vector <T> Tree[MAXN]; int n, q, DP[MAXN][MAXN]; bool Visited[MAXN]; void DFS(int Father, int Node) { for (int i = 0; i < Tree[Node].size(); i ++) { T Son = Tree[Node][i]; if (Son.first != Father and Visited[Son.first] == false) { DFS(Node, Son.first); for (int j = q; j > 0; j --) { for (int k = j - 1; k >= 0; k --) { DP[Node][j] = max(DP[Node][j], Son.second + DP[Son.first][k] + DP[Node][j - k - 1]); } } } } return ; } int main () { scanf ("%d %d", &n, &q); for (int i = 1; i < n; i ++) { int u, v, w; scanf ("%d %d %d", &u, &v, &w); Tree[u].push_back(make_pair(v, w)); Tree[v].push_back(make_pair(u, w)); } DFS(1, 1); printf ("%d\n", DP[1][q]); return 0; }
-
未完待续