树形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;
      }
      
      

      别喷我的压行啊!!!

posted @ 2021-10-18 16:45  佐世保の时雨酱  阅读(85)  评论(2编辑  收藏  举报