树上背包
树上的背包问题,也就是背包问题与树形 DP 的结合。
树上背包往往是设
进行状态转移时,依次进入每一个子节点
时间复杂度貌似是
但实际上每个节点的背包容量为子树的大小,如果在合并过程中计算子树大小,每次合并相当于是将
这个合并时间可以形象化地转化为在
如果限制选的数量不能超过
证明见 子树合并背包类型的dp的复杂度证明。
例题:P2014 [CTSC1997] 选课
有
门课程,第 门课程的学分为 ,每门课程有零门或一门先修课,有先修课的课程需要先学完其先修课,才能学习该课程。一位学生要学习 门课程,求其能获得的最多学分数。
数据范围:
分析:由于每门课最多只有一门先修课,与有根树中一个点最多只有一个父亲节点的特点类似。可以利用这个性质来建树,从而所有课程形成了一个森林结构。为了方便起见,可以新增一门
设
转移的过程结合了树形 DP 和背包问题的特点,枚举点
将点
第二维可以通过滚动数组优化掉,此时需要倒序枚举
先修课这种关系是要求必须选一棵子树的根才能选子树中其他点的,所以枚举
参考代码
#include <cstdio> #include <vector> #include <algorithm> using std::vector; using std::min; using std::max; const int N = 305; vector<int> tree[N]; int n, m, s[N], sz[N], dp[N][N]; void dfs(int u) { sz[u] = 1; dp[u][1] = s[u]; for (int v : tree[u]) { dfs(v); for (int i = min(sz[u], m + 1); i >= 1; i--) { for (int j = min(sz[v], m + 1 - i); j >= 1; j--) { dp[u][i + j] = max(dp[u][i + j], dp[u][i] + dp[v][j]); } } sz[u] += sz[v]; } } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { int k; scanf("%d%d", &k, &s[i]); tree[k].push_back(i); } dfs(0); printf("%d\n", dp[0][m + 1]); return 0; }
例题:P3177 [HAOI2015] 树上染色
分析:显然设
但是这个值存什么呢?如果直接表示子树内黑点、白点间的收益,这个状态没有办法转移,因为子树内最大化收益的染色方案被合并上去后未必是最优的方案,也就是有后效性。
考虑每条边对最终答案的贡献,如果一条边的两侧有一对黑点或白点,则这条边对这两个点构成的路径是有贡献的。也就是说,一条边对总答案的贡献次数等于边的两侧同色点个数的乘积。而子树内每条边对总答案的贡献这个状态值在子树合并过程中是可以向上传递的。
因此
如果指定
参考代码
#include <cstdio> #include <vector> #include <algorithm> #include <utility> using std::vector; using std::pair; using std::min; using std::max; using edge = pair<int, int>; // 点,边权 using ll = long long; const int N = 2005; vector<edge> tree[N]; int n, k, sz[N]; ll dp[N][N]; void dfs(int u, int fa) { sz[u] = 1; for (edge e : tree[u]) { int v = e.first, w = e.second; if (v == fa) continue; dfs(v, u); for (int i = min(k, sz[u]); i >= 0; i--) { for (int j = min(k - i, sz[v]); j >= 0; j--) { // 子树内的黑点数量 int black = j * (k - j); // 子树内*子树外 int white = (sz[v] - j) * (n - sz[v] - (k - j)); // 子树内*子树外 ll c = 1ll * w * (black + white); dp[u][i + j] = max(dp[u][i + j], dp[u][i] + dp[v][j] + c); } } sz[u] += sz[v]; } } int main() { scanf("%d%d", &n, &k); for (int i = 1; i < n; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); tree[u].push_back({v, w}); tree[v].push_back({u, w}); } dfs(1, 0); printf("%lld\n", dp[1][k]); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?