最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划
题目描述
有 $N$ 件 物品 和一个 体积 为 $V$ 的 背包
第 $i$ 件 物品 的 体积 为 $v_i$, 价值 为 $w_i$
每件 物品 有一个 父节点物品 $p_i$,如果想选第 $i$ 件 物品,就必须选他的 父节点 $p_i$
拓扑结构 如下图所示:
如果选择 物品5,则必须选择 物品1 和 物品2。
这是因为 物品2 是 物品5 的 父节点,物品1 是 物品2 的 父节点。
求一个方案,使得该选择的物品合法,总体积不超过 $V$,且总价值最大
分析
这是一道 背包DP 的 变种题目
根据题设的 拓扑结构 可以观察出每个 物品 的关系构成了一棵 树
而以往的 背包DP 每个 物品 关系是 任意的(但我们一般视为 线性的)
所以,这题沿用 背包DP 的话,要从原来的 线性DP 改成 树形DP 即可
然后思考 树形DP 的 状态转移
先比较一下以往 线性背包DP 的 状态转移,第 $i$ 件 物品 只会依赖第 $i-1$ 件 物品 的状态
如果本题我们也采用该种 状态依赖关系 的话,对于节点 $i$,我们需要枚举他所有子节点的组合 $2^k$ 种可能
再枚举 体积,最坏时间复杂度 可能会达到 $O(N\times2^N\times V)$(所有子节点都依赖根节点)
最终毫无疑问会 TLE
因此我们需要换一种思考方式,那就是枚举每个 状态 分给各个子节点 的 体积
这样 时间复杂度 就是 $O(N \times V\times V)$
具体分析如下:
闫氏DP分析法
Code
时间复杂度: $O(N \times V \times V)$
#include <iostream>
#include <cstring>
using namespace std;
const int N = 110;
int n, m, root;
int h[N], e[N], ne[N], idx;
int v[N], w[N];
int f[N][N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
//先枚举所有状态体积小于等于j-v[u]的所有子节点们能够获得的最大价值
for (int i = h[u]; ~i; i = ne[i])
{
int son = e[i];
dfs(son); //从下往上算,先计算子节点的状态
for (int j = m - v[u]; j >= 0; -- j) //枚举所有要被更新的状态
{
for (int k = 0; k <= j; ++ k) //枚举该子节点在体积j下能使用的所有可能体积数
{
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
}
}
}
//最后选上第u件物品
for (int j = m; j >= v[u]; -- j) f[u][j] = f[u][j - v[u]] + w[u];
for (int j = 0; j < v[u]; ++ j) f[u][j] = 0; //清空没选上u的所有状态
}
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);
}
dfs(root);
cout << f[root][m] << endl;
return 0;
}
题解我只看铅笔哥!
看这个字,应该是铅笔姐
铅笔是男的
我一个男的字都被老师认成女的
hh
励志走遍每一个铅笔哥的tijie
请问dfs中为什么这里要倒序枚举啊?for (int j = m - v[u]; j >= 0; – j)
调用的上一层的 倒序枚举确保这一层的只被更新一次。
分组背包就是这样
但这个难道不是用的这一层的吗
用的不是f[u][j-k]吗
保证只加一次 w[u],因为后面的数据要依赖前面的数据
三年了, 还是要强答一发:
这个循环是在处理某一棵子树的各种选法, 而非对各个子树的枚举, 大佬肯定是没注意外重的循环(我也是)
orz
大佬的题解真的太清晰了!!!!!%%%%%%%
好文tql
照片里的“属性”和“集合”是不是写反了?
为什么是2^k的选法啊
每种物体有两种选法,有k个物体
j循环的倒序是由于省去了子树这一维,f[u][j-k]是不考虑本子树,考虑前面所有son,体积达到j-k的情况,类比于01背包的优化一维
树形dp和分组背包的结合
%%% or2
for (int j = m; j >= v[u]; – j) f[u][j] = f[u][j - v[u]] + w[u];
for (int j = 0; j < v[u]; ++ j) f[u][j] = 0; //清空没选上u的所有状态
这里的思路不是很懂
兄弟你懂了吗 我也不是懂
这个字也太好看了%%%%%
’‘’void dfs(int u)
{
for(int i=h[u];i != -1;i = ne[i])
{
int son= e[i];//遍历主件
dfs(son);
for(int j=v[u];j<=m;j)
{
for(int k=0;k<=j-v[u];k)
{
dp[u][j] = max(dp[u][j],dp[u][j-k]+dp[son][k]);
}
}
}’‘’
我就只改动了最后枚举体积和物品的情况,请问是哪里不对呀233
以为是二叉树
请问最后选上第u件物品,为啥要从大到小枚举呢?
因为要用小的来更新大的,不能用已经更新的去更新别的
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; 有大佬能说一下这里e[idx] ,ne[idx],h[a],idx分别是什么吗
我的理解是e数组存储的是节点的值,ne存储的是当前节点的下一个节点,h存储的是头节点的位置,idx简单理解成指针
数组的本质其实是个映射,
h数组的映射是 点的编号 -> 最新添加的出边的编号的映射(因为是头插法),
e数组的映射关系是,入边的编号 -> 这条边指向的点的编号的映射,
ne数组是,边 -> 下一个同样起点的边的映射,
idx表示边的编号,每次用一条就++,保证所有边的编号不一样,最后idx就代码这张图有多少条边。
图的集合和属性写反了233
请问这里为什么要从 m-v[u] 开始呢,既然选择了u,那么一定选择了 u 的父结点,从第二层就选不到那么大了吧~~
是从下往上算的,是一定从底下判断一定会选u,如果太大了,上面就不会被转移,这种状态自然会被淘汰,这里只是枚举了一种上限的情况,不重不漏,如果u就是根节点,那就没问题了
加油!
加油!
大佬请问一下dfs函数中最后为什么要把小于v[u]的地方全部置成0?
因为这是一个有依赖的背包,选附件有价值的前提是至少要选上主件