疯狂的背包问题(14) - 有依赖的背包问题
树形依赖背包:dp[u][j]表示子树u容量j的最大价值,递归时先选u再对子节点分配容量做类分组背包合并。
OJ: luogu
题目 ID: U661996
难度:普及+/提高-
标签:动态规划背包树形DP有依赖的背包
日期: 2026-08-08 23:13
题意
思路
一句话本质:必须先选父才能选子,依赖形成树形结构——对每个节点
先看暴力:
import sys
def solve() -> None:
data = list(map(int, sys.stdin.buffer.read().split()))
it = iter(data)
N = next(it)
V = next(it)
v = [0] * (N + 1)
w = [0] * (N + 1)
parent = [0] * (N + 1)
for i in range(1, N + 1):
v[i] = next(it)
w[i] = next(it)
parent[i] = next(it)
ans = 0
for mask in range(1 << N):
selected = [False] * (N + 1)
ok = True
for i in range(N):
if mask & (1 << i):
selected[i + 1] = True
for i in range(1, N + 1):
if not selected[i]:
continue
p = parent[i]
while p != -1:
if not selected[p]:
ok = False
break
p = parent[p]
if not ok:
break
if not ok:
continue
vol = 0
val = 0
for i in range(1, N + 1):
if selected[i]:
vol += v[i]
val += w[i]
if vol <= V:
ans = max(ans, val)
print(ans)
if __name__ == '__main__':
solve()暴力枚举所有
依赖是树形的,怎么用 DP?
假设我们以节点
dp[u][j] 的含义是什么?
定义
怎么初始化 dp[u]?
首先把
怎么把子节点 c 合并到 u 上?
处理完子节点
对于一个容量为
这里
这个合并和分组背包有什么关系?
每个子节点
因此合并每个子节点时,需要用到分组背包的"旧状态隔离"思想:dp[u][j - k] 应该来自处理当前子节点之前的旧状态,而不是被当前子节点反复更新的新状态。所以 dp[u][j - k] 是上一轮(上一个子节点合并后)的状态。
为什么要引入虚拟根 0?
题面中
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 105;
int N, V;
int v[maxn], w[maxn]; // 每个节点的体积和价值
vector<int> children[maxn]; // 树形依赖关系
// dp[u][j] 表示以 u 为根的子树,占用容量 j 时的最大价值。
int dp[maxn][maxn];
void dfs(int u) {
// 先把节点 u 本身的价值放入(必须选 u 才能选其子节点)。
for (int j = v[u]; j <= V; j++)
dp[u][j] = w[u];
for (int c : children[u]) {
dfs(c);
// 给子节点分配容量,类似分组背包:子节点只能选一个「分配量」。
for (int j = V; j >= v[u]; j--)
for (int k = 0; k <= j - v[u]; k++)
dp[u][j] = max(dp[u][j], dp[u][j - k] + dp[c][k]);
}
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
cin >> N >> V;
for (int i = 1; i <= N; i++) {
int p;
cin >> v[i] >> w[i] >> p;
if (p == -1) { // 根节点挂在虚拟根 0 下
children[0].push_back(i);
} else {
children[p].push_back(i);
}
}
dfs(0);
cout << dp[0][V] << '\n';
return 0;
}复杂度
时间
空间
总结
有依赖的背包是"树形 DP + 背包容量分配"的结合。核心是两点:先保证根节点被选(