树形DP

树形DP

在树上跑 DP
此随笔仅放有根树的DP,换根DP传送门

相关资料

知乎 树形动态规划详解

例题

树上简单DP 洛谷 P1352 没有上司的舞会

题意
有 n 个员工,员工之间的上下级关系构成一棵树
现有一个舞会邀请员工参加,员工 i 的到来会给舞会增加一定的快乐指数 r[i]
但是没有员工愿意在舞会上遇到自己的上司,所以如果他知道自己的直接上司参加了舞会,他将会坚决不参加舞会
问你如何邀请员工使得舞会的快乐指数最大?

思路
每名员工的到来会受到上司的制约,如果直接上司来,那么该员工一定不来;如果直接上司不来,那么该员工可来可不来
可以利用 DP 求解

状态
dp[i][0] 表示员工 i 不参加舞会
dp[i][1] 表示员工 i 参加舞会

转移
由题可知必定存在一个员工 root 没有直接上司,将其作为 dfs 的起点
对于员工 i 的所有直接下属 j
\(dp[i][1] += dp[j][0]\)
\(dp[i][0] += max(dp[j][0], dp[j][1])\)

最终答案即为 $max(dp[root][0], dp[root][1]) $

代码

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << '\n'
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n'
//#define int long long

using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*
1 代表当前去,0 代表当前不去
*/
const int maxm = 6e3 + 5, inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f, mod = 998244353;
int n, r[maxm], dp[maxm][2], in[maxm];
vector<int> e[maxm];

void dfs(int u, int fa){
	dp[u][1] = r[u];
	dp[u][0] = 0;
	for(auto v : e[u]){
		if(v != fa){
			dfs(v, u);
			dp[u][1] += dp[v][0];
			dp[u][0] += max(dp[v][0], dp[v][1]);
		}
	}
	return ;
}

void solve(){
	cin >> n;
	for(int i = 1; i <= n; ++ i) cin >> r[i];
	for(int i = 1; i < n; ++ i){
		int u, v;
		cin >> u >> v;
		e[v].push_back(u);
		++ in[u];
	}
	int root = 0;// 找唯一上司
	for(int i = 1; i <= n; ++ i)
		if(in[i] == 0){
			root = i; break;
		}
	dfs(root, -1);
	cout << max(dp[root][0], dp[root][1]) << '\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	int _ = 1;
	// cin >> _;
	while(_ --){
		solve();
	}
	return 0;
}

树上背包 洛谷 P2014 [CTSC1997] 选课

题意
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 \(N\) 门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b)。一个学生要从这些课程里选择 \(M\) 门课程学习,问他能获得的最大学分是多少?

思路
如果说课与课之间没有先后的制约关系,那么直接跑背包就可以解决这个问题
但是有了这个制约,我们就需要在制约关系形成的树上跑背包问题

为了方便,我们将没有前置课程的课均视为前置课程为 0,并且第 0 门课的学分为 0

状态
\(dp[i][j][0/1]\) 表示节点 i 及其子树选择 j 节课所能得到的最大学分,0 表示不选第 i 门课,1 表示选
转移
在 dfs 整棵树的过程中,我们顺便维护 dp 数组

  • 如果说当前节点选择了,那么它的后续课程可以选,也可以不选

对于节点 u 跑容量为 m 的 01 背包,枚举大小 k 以达到当前最优
$dp[u][i][1] = max(dp[u][i][1], dp[u][i - k][1] + max(dp[v][k][0], dp[v][k][1])); $

  • 如果说当前节点不选,那么它的后续课程不可以选

当 u = 0 即为虚拟的第 0 节课时,它的后续课程的选择无限制 \(dp[u][i][0] = max(dp[u][i][0], dp[u][i - k][0] + max(dp[v][k][0], dp[v][k][1]));\)
当课程 u 为普通课程时,它的后续课程无法选择 \(dp[u][i][0] = max(dp[u][i][0], dp[u][i - k][0] + dp[v][k][0]);\)

最后在将当前课程的价值放入 dp 数组中即可

详见代码

代码

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << '\n'
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n'
//#define int long long

using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*
dp[i][j][0/1] 表示节点 i 及其子树选择 j 节课所能得到的最大学分,0 表示不选第 i 门课,1 表示选
*/
const int maxm = 3e2 + 5, inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f, mod = 998244353;
int n, m, dp[maxm][maxm][2], v[maxm];
vector<pii> e[maxm];

void dfs(int u, int fa){
	for(auto [s, v] : e[u]){
		if(v != fa){
			dfs(v, u);
			for(int i = m; i >= 0; -- i){
				for(int k = 0; k <= i; ++ k){
					dp[u][i][1] = max(dp[u][i][1], dp[u][i - k][1] + max(dp[v][k][0], dp[v][k][1]));
					if(u == 0) dp[u][i][0] = max(dp[u][i][0], dp[u][i - k][0] + max(dp[v][k][0], dp[v][k][1]));
					else dp[u][i][0] = max(dp[u][i][0], dp[u][i - k][0] + dp[v][k][0]);
				}
			}
		}
	}
	for(int i = m; i; -- i) dp[u][i][1] = dp[u][i - 1][1] + v[u];
	dp[u][0][1] = 0;
	return ;
}

void solve(){
	cin >> n >> m;
	for(int i = 1; i <= n; ++ i){
		int k;
		cin >> k >> v[i];
		e[k].push_back({v[i], i});
	}
	dfs(0, -1);// 0 视为根节点,没有前置课程的课均视为前置课程为 0
	cout << max(dp[0][m][0], dp[0][m][1]) << '\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	int _ = 1;
	// cin >> _;
	while(_ --){
		solve();
	}
	return 0;
}

综合运用

状态
\(dp[i][j]\) 表示节点 i 及其子树保留 j 条边时的苹果数量最大值

转移
遍历每个子节点的时候,枚举其内保留的边数,枚举当前子树保留的边数大小取最优即可
$dp[u][i] = max(dp[u][i], dp[u][i - j - 1] + dp[v][j] + w); $

Qiansui_code


状态
$dp[i][j] $ 表示子树 i 中选 j 个叶子节点(即用户)所能得到的最大收益

转移
对于节点 u 的所有后代 v,相当于跑分组背包

for(int i = sz[u]; i >= 0; -- i){// 跑分组背包
	for(int j = 0, lim = min(i, sz[v]); j <= lim; ++ j){// 分配给子树 v 的节点数不超过其叶子节点个数
		dp[u][i] = max(dp[u][i], dp[u][i - j] + dp[v][j] - w);
	}
}

Qiansui_code

posted on 2023-10-06 22:35  Qiansui  阅读(8)  评论(0编辑  收藏  举报