树形DP小结
\(\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\) 需要倒序遍历。
代码:
点击查看代码
// ============================================= // 暁の水平线に胜利を刻むのです! // Author: 佐世保の时雨 // Blog: https://www.cnblogs.com/SasebonoShigure // ============================================= // 此代码为2019年码风 #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; }
-
\(Luogu\ P4516\ [JSOI2018]\)潜入行动:
这道题也是一道书上背包的
简单好题,我们可以定义 \(\large f_{Root,i,0/1,0/1}\) 表示在 \(Root\) 的子树中放置了 \(i\) 个监听设备,\(Root\) 是否放置监听设备,\(Root\) 是否被监听的方案数。经过简单的推理,我们可以得出状态转移方程:(推出状态转移方程的过程之后补)
其实也不是很长,对吧
\(\large{ f_{Root,i+j,0,0}=\sum f_{Root,i,0,0}\times f_{v,j,0,1}\\ f_{Root,i+j,1,0}=\sum f_{Root,i,0,0}\times (f_{v,j,0,0}+f_{v,j,0,1})\\ f_{Root,i+j,0,1}=\sum f_{Root,i,0,1}\times (f_{v,j,0,1}+f_{v,j,1,1})+f_{Root,i,0,0}\times f_{v,j,1,1}\\ f_{Root,i+j,1,1}=\sum f_{Root,i,1,0}\times (f_{v,j,1,0}+f_{v,j,1,1})+f_{Root,i,1,1}\times (f_{v,j,0,0}+f_{v,j,0,1}+f_{v,j,1,0}+f_{v,j,1,1})\\ }\)
代码:
点击查看代码
// ============================================= // 暁の水平线に胜利を刻むのです! // Author: 佐世保の时雨 // Blog: https://www.cnblogs.com/SasebonoShigure // ============================================= // 此代码为2019年码风 #include <cstdio> #include <vector> #include <cstring> using namespace std; const int MAXN = 100005; const long long MOD = 1000000007ll; vector<int> Tree[MAXN]; int Read() { int x = 0, f = 0; char c = getchar (); while (c > '9' or c < '0') { if (c == '-') { f = 1; } c = getchar (); } while (c >= '0' and c <= '9') { x = (x << 1) + (x << 3) + (c ^ 48); c = getchar (); } return (f == 1) ? -x : x; } void Write(const int &x) { if (x < 0) { putchar('-'); Write(-x); } if (x > 10) { Write(x / 10); } putchar(x % 10 + 48); return ; } int n, k, u, v, DP[MAXN][105][2][2], DP2[105][2][2], Size[MAXN]; void DFS(long long Node,long long Father) { DP[Node][0][0][0] = DP[Node][1][1][0] = Size[Node] = 1; for (int i = 0; i < Tree[Node].size(); i ++ ) { int Child = Tree[Node][i]; if(Child == Father) { continue; } DFS(Child, Node); memset(DP2, 0, sizeof DP2); for (int j = 0; j <= k and j <= Size[Node]; j ++ ) { for (int l = 0; l <= k - j and l <= Size[Child]; l ++ ) { DP2[j + l][0][0] = (DP2[j + l][0][0] + (((long long)DP[Node][j][0][0] * DP[Child][l][0][1]) % MOD)) % MOD; DP2[j + l][1][0] = (DP2[j + l][1][0] + (((long long)DP[Node][j][1][0] * DP[Child][l][0][1] + (long long)DP[Node][j][1][0] * DP[Child][l][0][0]) % MOD)) % MOD; DP2[j + l][0][1] = (DP2[j + l][0][1] + (((long long)DP[Node][j][0][1] * DP[Child][l][0][1] + (long long)DP[Node][j][0][1] * DP[Child][l][1][1] + (long long)DP[Node][j][0][0] * DP[Child][l][1][1]) % MOD)) % MOD; DP2[j + l][1][1] = (DP2[j + l][1][1] + (((long long)DP[Node][j][1][1] * DP[Child][l][0][0] + (long long)DP[Node][j][1][1] * DP[Child][l][0][1] + (long long)DP[Node][j][1][1] * DP[Child][l][1][1] + (long long)DP[Node][j][1][0] * (long long)DP[Child][l][1][0] + (long long)DP[Node][j][1][0] * DP[Child][l][1][1] + (long long)DP[Node][j][1][1] * DP[Child][l][1][0]) % MOD)) % MOD; } } Size[Node] += Size[Child]; memcpy (DP[Node], DP2, sizeof DP2); } return ; } int main() { n = Read(); k = Read(); for (int i = 1; i < n; i ++ ) { u = Read(); v = Read(); Tree[u].push_back(v); Tree[v].push_back(u); } DFS(1, 0); printf ("%lld\n", (DP[1][k][0][1] + DP[1][k][1][1]) % MOD); return 0; }
别喷我的压行啊!!!
-