树上背包(P2014 [CTSC1997] 选课)
树上的背包问题,简单来说就是背包问题与树形 DP 的结合。
树上背包问题概述
树上背包问题指的是在树形结构上进行动态规划,结合背包问题的思想,通常用于解决子树资源分配或依赖选择的问题。这类问题通常涉及节点间的依赖关系,需要在树上进行状态转移。
基本思路
- 状态定义:通常定义
dp[u][j]
表示以节点u
为根的子树中,选择j
个节点(或消耗j
单位资源)时的最优解(如最大价值或最小代价)。 - 转移方程:通过遍历子节点,将子节点的状态合并到父节点中。对于每个子节点
v
,枚举父节点和子节点的分配情况,更新状态。
代码框架(C++)
以下是树上背包问题的通用代码框架:
#include <vector>
#include <algorithm>
using namespace std;const int N = 1e3 + 5; // 节点数
const int M = 1e3 + 5; // 背包容量
vector<int> tree[N];
int dp[N][M]; // dp[u][j]: 以u为根的子树,容量为j时的最优解
int n, m; // 节点数和背包容量void dfs(int u, int parent) {// 初始化,通常将当前节点u的状态初始化为某种值for (int j = 0; j <= m; ++j) {dp[u][j] = ...; // 根据问题初始化}for (int v : tree[u]) {if (v == parent) continue;dfs(v, u); // 递归处理子节点// 合并子节点状态到父节点for (int j = m; j >= 0; --j) { // 注意逆序枚举,避免重复计算for (int k = 0; k <= j; ++k) {dp[u][j] = max(dp[u][j], dp[u][j - k] + dp[v][k]);}}}
}
关键点
- 递归处理子树:通过DFS遍历树,确保子节点的状态先于父节点计算。
- 逆序枚举背包容量:避免重复计算,类似于01背包的优化方式。
- 状态合并:将子节点的状态通过枚举分配方式合并到父节点中。
示例问题:子树节点选择
假设每个节点有一个价值 val[u]
和体积 w[u]
,要求在子树中选择节点,使得总体积不超过 m
,且总价值最大。
int val[N], w[N]; // 节点的价值和体积void dfs(int u, int parent) {// 初始化:选择当前节点ufor (int j = w[u]; j <= m; ++j) {dp[u][j] = val[u];}for (int v : tree[u]) {if (v == parent) continue;dfs(v, u);// 合并子节点状态for (int j = m; j >= w[u]; --j) {for (int k = 0; k <= j - w[u]; ++k) {dp[u][j] = max(dp[u][j], dp[u][j - k] + dp[v][k]);}}}
}
复杂度分析
- 时间复杂度:
O(n * m^2)
,其中n
是节点数,m
是背包容量。通过优化可以降为O(n * m)
。 - 空间复杂度:
O(n * m)
。
优化技巧
子树大小优化:限制背包容量的枚举范围为子树大小,减少无效计算。
int size[N]; // 记录子树大小void dfs(int u, int parent) {size[u] = 1;for (int j = 0; j <= m; ++j) dp[u][j] = ...;for (int v : tree[u]) {if (v == parent) continue;dfs(v, u);size[u] += size[v];for (int j = min(m, size[u]); j >= 0; --j) {for (int k = 0; k <= min(j, size[v]); ++k) {dp[u][j] = max(dp[u][j], dp[u][j - k] + dp[v][k]);}}} }
滚动数组:如果空间紧张,可以用滚动数组优化空间。
常见应用场景
- 树上依赖背包问题(如选择子树中的节点需满足依赖关系)。
- 树形结构上的资源分配问题(如带宽分配、任务调度)。
- 子树统计问题(如统计满足条件的子树数量)。
P2014 [CTSC1997] 选课
思路:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<iostream>
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int n, m;
struct {int i, next;//i这次可以不用
}a[302];
int h[302];
int head[302];
int dp[302][302];
void dfs(int i) {while (h[i] != -1) {dfs(a[h[i]].i);for (int j = m; j >= 0; j--) {for (int z = j; z >= 0; z--) {dp[i][j] = max(dp[i][j], dp[h[i]][z] + dp[i][j - z]);}}h[i] = a[h[i]].next;}for (int j = m; j >= 1; j--) {dp[i][j] = dp[i][j - 1] + head[i];}
}
int main(){ios::sync_with_stdio(false); // 禁用同步cin.tie(nullptr); // 解除cin与cout绑定cin >> n >> m;memset(h, -1, sizeof(h));int l;for (int i = 1; i <= n; i++) {cin >> l >> head[i];a[i].i = i;a[i].next = h[l];h[l] = i;}m = m + 1;//0当成一门课且必选dfs(0);cout << dp[0][m] << endl;return 0;
}