树形DP小结

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} 3i=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,ij1+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 uv 这条边上的苹果数。

      注意: 由于这是一个 0 / 1 0/1 0/1 背包,所以 i 、 j i、j ij 需要倒序遍历。

      代码:

      // =============================================
      // 暁の水平线に胜利を刻むのです!
      // 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,ij),剩余细节就不用讲了吧。

      代码:

      // =============================================
      // 暁の水平线に胜利を刻むのです!
      // 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 也是一知半解,所以这篇小结就只能草草收尾了,如果以后有时间的话我会对这篇博客进行修改补充。

完结撒花!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值