有 N 个物品和一个容量是 V 的背包。

物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。

如下图所示:

如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。

每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

第一行有两个整数 N,V,用空格隔开,分别表示物品个数和背包容量。

接下来有 N 行数据,每行数据表示一个物品。 第 i 行有三个整数 vi,wi,pi,用空格隔开,分别表示物品的体积、价值和依赖的物品编号。 如果 pi=−1,表示根节点。 数据保证所有物品构成一棵树。

输出格式 输出一个整数,表示最大价值。

数据范围 1≤N,V≤100 1≤vi,wi≤100 父节点编号范围:

内部结点:1≤pi≤N; 根节点 pi=−1;

5 7
2 3 -1
2 2 1
3 5 1
4 7 2
3 6 2
11

思路

选一个物品必须选其父节点, 也就构成了树形DP。 对于根节点root, 它的子节点选法构成了一个分组背包问题。 对于每组的选法, 如果只能选一个的话就用机器分配 的单选方法, 即模板选法。 如果能选一些则可以使用 金明的预算方案的二进制状态压缩方法枚举所有。

这里则不能二进制状态压缩, 因为1 << 100 会超时, 故需采用体积划分法。 即对于每个子树, 先确定能用的最大体积, 然后再依次为基础枚举所有能用的体积来转移。

先比较一下以往 线性背包DP 的 状态转移,第 i 件 物品 只会依赖第 i−1 件 物品 的状态

如果本题我们也采用该种 状态依赖关系 的话,对于节点 i,我们需要枚举他所有子节点的组合 2k 种可能

再枚举 体积,最坏时间复杂度 可能会达到 (所有子节点都依赖根节点) 最终毫无疑问会 TLE

因此我们需要换一种思考方式,那就是枚举每个 状态 分给各个子节点 的 体积

这样 时间复杂度 就是

for(int i = m - v[u]; i >= 0; i--)
	for(int j = 0; j <= i; j++)
		f[u][i] = max(f[u][i], f[u][i - j] + f[son][j]);

最后再加上root的物品。

状态表示: f[i][j] 以i为根, 背包容量为 j 的最大价值 **

代码

#include <iostream>
#include <cstring>
#include <string>
 
using namespace std;
 
const int N = 110, M = N * 2;
int h[N], e[M], v[M], w[M],ne[M], idx;
int n,m;
int f[N][N]; // 以i为根的树, 背包容量为j 时的最大价值
int root;
 
void add(int a, int b)
{
    e[idx] = b,ne[idx] = h[a], h[a] = idx++;
}
 
void dp(int u)
{
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        dp(j);
        
        for(int k = m - v[u]; k >= 0; k--)
            for(int q = 0; q <= k; q++)
                f[u][k] = max(f[u][k], f[u][k - q] + f[j][q]);
    }
    for(int i = m; i >= v[u]; i--)
        f[u][i] = f[u][i - v[u]] + w[u];
    for(int i = 0; i < v[u]; i++)
        f[u][i] = 0;
}
 
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int p;
        cin >> v[i] >> w[i] >> p;
        if(p == -1) root = i;
        else {
            add(p, i);
        }
    }
    dp(root);
    cout << f[root][m] << endl;
    return 0;
}