Warning: \large\texttt{Warning:} Warning: 此篇博客中的代码是本人在 2019 2019 2019 年到 2021 2021 2021 年间断断续续写的,所以码风有较大的差异 ,后期会更改代码。
树形 D P DP DP
只要你学会了树,还学会了 d p dp dp ,那么你就学会了树形 d p dp dp 。 B y By By 某不愿透露姓名的教练
-
什么是树形 D P DP DP
树形 D P DP DP,就是在“树”的数据结构上的动态规划,一般状态转移都是和子树相关,且能与线段树等数据结构相结合。
因为其具有传递性,就对于具有一定规律的树上问题求解起到了很大帮助。
-
常见的题型
-
子树和计数:
这类问题主要是统计子树和,通过加减一些子树满足题目中要求的某些性质
例如 C F 767 C CF767C CF767C、 L u o g u P 1122 Luogu\ P1122 Luogu P1122
-
树上背包问题:
这类问题就是让你求在树上选一些点满足价值最大的问题,一般都可以设 f i , j \large f_{i,j} fi,j 表示 i i i 这颗子树选 j j j 个点的最优解。
例如 L u o g u P 1272 P 1273 Luogu\ P1272\ P1273 Luogu P1272 P1273
-
花费最少的费用覆盖所有点:
这类问题是父亲与孩子有联系的题。基本有两种类型:
-
选父亲必须不能选孩子(强制)
-
选父亲可以不用选孩子(不强制)
例如 U V A 1220 UVA\ 1220 UVA 1220(类型1)、 L u o g u P 2458 Luogu\ P2458 Luogu P2458(类型2)
-
-
树上统计方案数:
这类问题就是给你一个条件,问你有多少个点的集合满足这样的条件。这类题主要运用乘法原理,控制一个点不动,看他能做多少贡献
-
与多种算法结合:
这类问题就只能根据题目分析,听天由命了 ⋯ ⋯ \cdots\cdots ⋯⋯
-
-
例题:
-
C F 767 C G a r l a n d CF767C\ Garland CF767C Garland
这道题就是典型的统计子树和的问题。但是这道题并不需要进行复杂的 d p dp dp ,只需要统计子树中的和,若达到了 ∑ i = 1 n a i 3 \large\dfrac{\sum^n_{i=1}a_i}{3} 3∑i=1nai 后就直接删去这条边,并记录下来。最后判断即可。
代码:
// ============================================= // 暁の水平线に胜利を刻むのです! // Author: 佐世保の时雨 // Blog: https://www.cnblogs.com/SasebonoShigure // ============================================= // 此代码为2019年-2020年初码风 #include <cstdio> #include <vector> #include <algorithm> using namespace std; const int MAXN = 1000010; typedef long long LL; int n, Root, Ans[MAXN], v, cnt; LL Size[MAXN], Light[MAXN], Sum; vector <int> Tree[MAXN]; void DFS(int Node, int Father) { for (int i = 0; i < Tree[Node].size(); i ++ ) { int Child = Tree[Node][i]; if (Child == Father) { continue; } DFS(Child, Node); Light[Node] += Light[Child]; } if (Light[Node] == Sum) { Ans[++ cnt] = Node; Light[Node] = 0; } return ; } int main () { scanf ("%d", &n); for (int i = 1; i <= n; i ++ ) { scanf ("%d %lld", &v, &Light[i]); Sum += Light[i]; if (v == 0) { Root = i; } else { Tree[v].push_back(i); Tree[i].push_back(v); } } if (Sum % 3 != 0) { printf ("-1\n"); return 0; } Sum /= 3; DFS(Root, -1); if (cnt <= 2) { printf ("-1\n"); return 0; } printf ("%d %d\n", Ans[1], Ans[2]); return 0; }
-
L u o g u P 2015 Luogu\ P2015 Luogu P2015 二叉苹果树:
这道题属于常见题型中的树上背包问题,可以将其作为模板题。
这道题还有一个隐含的条件,当某条边被保留下来时,从根节点到这条边的路径上的所有边也都必须保留下来。
所以,我们可以很容易定义我们的 d p dp dp 状态。令 f i , j \large f_{i,j} fi,j 表示在 i i i 子树中保留 j j j 条边能够得到的最大苹果树。
那么,状态转移方程就显而易见了: f u , i = max ( f u , i , f u , i − j − 1 + f v , j + A p p l e u , v ) \large\mathcal{ f_{u,i}=\max(f_{u,i},f_{u,i-j-1}+f_{v,j}+Apple_{u,v})} fu,i=max(fu,i,fu,i−j−1+fv,j+Appleu,v) ,其中 v v v 为 u u u 的子节点, A p p l e u , v \mathcal{Apple_{u,v}} Appleu,v 表示 u → v u \rightarrow v u→v 这条边上的苹果数。
注意: 由于这是一个 0 / 1 0/1 0/1 背包,所以 i 、 j i、j 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; }
-
L u o g u P 1272 Luogu\ P1272 Luogu P1272 重建道路:
作为树上背包问题模板题之一,我们可以很快定义出 d p dp dp 状态: f i , j \large f_{i,j} fi,j 表示在 i i i 的子树中分离出 j j j 个节点所需要割去的最小边数。对于每个点 u u u,它既可以包含于剩下的子树中,也可以不包含在这之中。由此,我们可以得到状态转移方程: f x , i = min ( f x , i + 1 , f x , j + f v , i − j ) \large f_{x,i}=\min(f_{x,i}+1,f_{x,j}+f_{v,i-j}) fx,i=min(fx,i+1,fx,j+fv,i−j),剩余细节就不用讲了吧。
代码:
// ============================================= // 暁の水平线に胜利を刻むのです! // Author: 佐世保の时雨 // Blog: https://www.cnblogs.com/SasebonoShigure // ============================================= // 此代码为2021年中下旬码风 #include <set> #include <queue> #include <cstdio> #include <iostream> #include <algorithm> using namespace std; const int Maxn = 2e2 + 10; #define LL long long int T, n, m, Root, u, v, Answer; int f[Maxn][Maxn], Deg[Maxn]; int Head[Maxn], Total = 1; int Next[Maxn << 2], To[Maxn << 2]; inline void Addedge (const int u, const int v) { To[++ Total] = v, Next[Total] = Head[u], Head[u] = Total; return ; } inline void Search (const int Index) { for (int i = 1; i <= m; ++ i ) { f[Index][i] = 0x3f3f3f3f; } f[Index][1] = 0; for (int i = Head[Index]; i; i = Next[i] ) { Search (To[i]); for (int j = m; j; -- j ) { int Val = f[Index][j] + 1; for (int k = 1; k < j; ++ k ) { Val = min (Val, f[Index][j - k] + f[To[i]][k]); } f[Index][j] = Val; } } return ; } signed main () { while (scanf ("%d %d", &n, &m) != EOF ) { for (int i = Total = 1; i <= n; ++ i ) { Head[i] = Deg[i] = 0; } for (int i = 1; i < n; ++ i ) { scanf ("%d %d", &u, &v); Addedge (u, v), ++ Deg[v]; } for (int i = 1; i <= n; ++ i ) { if (!Deg[i] ) { Root = i; } } Search (Root), Answer = f[Root][m]; for (int i = 1; i <= n; ++ i ) { Answer = min (Answer, f[i][m] + 1); } printf ("%d\n", Answer); } return 0; }
-
L u o g u P 4516 [ J S O I 2018 ] Luogu\ P4516\ [JSOI2018] Luogu P4516 [JSOI2018]潜入行动:
这道题也是一道书上背包的
简单好题,我们可以定义 f R o o t , i , 0 / 1 , 0 / 1 \large f_{Root,i,0/1,0/1} fRoot,i,0/1,0/1 表示在 R o o t Root Root 的子树中放置了 i i i 个监听设备, R o o t Root Root 是否放置监听设备, R o o t Root Root 是否被监听的方案数。经过简单的推理,我们可以得出状态转移方程:(推出状态转移方程的过程之后补)
其实也不是很长,对吧
f R o o t , i + j , 0 , 0 = ∑ f R o o t , i , 0 , 0 × f v , j , 0 , 1 \large{f_{Root,i+j,0,0}=\sum f_{Root,i,0,0}\times f_{v,j,0,1}} fRoot,i+j,0,0=∑fRoot,i,0,0×fv,j,0,1
f R o o t , i + j , 1 , 0 = ∑ f R o o t , i , 0 , 0 × ( f v , j , 0 , 0 + f v , j , 0 , 1 ) \large{f_{Root,i+j,1,0}=\sum f_{Root,i,0,0}\times(f_{v,j,0,0}+f_{v,j,0,1})} fRoot,i+j,1,0=∑fRoot,i,0,0×(fv,j,0,0+fv,j,0,1)
f R o o t , i + j , 0 , 1 = ∑ f R o o t , i , 0 , 1 × ( f v , j , 0 , 1 + f v , j , 1 , 1 ) + f R o o t , i , 0 , 0 × f v , j , 1 , 1 \large{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}} fRoot,i+j,0,1=∑fRoot,i,0,1×(fv,j,0,1+fv,j,1,1)+fRoot,i,0,0×fv,j,1,1
f R o o t , i + j , 1 , 1 = ∑ f R o o t , i , 1 , 0 × ( f v , j , 1 , 0 + f v , j , 1 , 1 ) + f R o o t , i , 1 , 1 × ( f v , j , 0 , 0 + f v , j , 0 , 1 + f v , j , 1 , 0 + f v , j , 1 , 1 ) \large{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})} fRoot,i+j,1,1=∑fRoot,i,1,0×(fv,j,1,0+fv,j,1,1)+fRoot,i,1,1×(fv,j,0,0+fv,j,0,1+fv,j,1,0+fv,j,1,1)
代码(以后会改):
// ============================================= // 暁の水平线に胜利を刻むのです! // Author: 佐世保の时雨 // Blog: https://www.cnblogs.com/SasebonoShigure // ============================================= // 此代码为2019年-2020年初码风 #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; }
别喷我的压行啊!!!
-
由于本人太菜,对于树形 d p dp dp 也是一知半解,所以这篇小结就只能草草收尾了,如果以后有时间的话我会对这篇博客进行修改补充。
完结撒花!!!