依赖背包
问题重述
有 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;
思路分析:
依赖背包和之前的背包问题都有所不同它是说物品之间有了一些依赖关系牵一发而动全身选一个则要把它的所有父以上点全部选入。我们来拆解一下我们的问题,和01背包一样我们先想办法找一个合适的当前的状态并尝试看看能不能转移它,继承01背包的思想我们依旧选择一个二维数组 ans[i][j] 来表示当前状态在结合一下具体问题01背包 ans[i][j] 表示的是前 i 个物品体积不超过 j 的情况下的最优解,而我们的依赖背包的物品是树中节点的关系,比较合适的就是用 ans[i][j] 来表示在选定节点 i 然后合法的选取其子节点的情况下体积不超过 j 的最优解。这就是我们的子问题显然我们最终要求解的是 ans[root][V] 即在选定根节点在选其子节点的情况下体积不超过V的最优解。那么现在我们来尝试看看这个状态能不能转移。
状态转移:
我们先来考虑边界条件,边界条件必要的有一个即 i 为叶节点的时候这个时候它没有子节点可选,那么此时的 ans[i][j] 的值就是在 j 大于等于节点 i 所代表的物品的体积时就为此物品的价值否则就为零。除此之外其实还有一个非必要的边界条件,即 i 所代表的物品的体积恰好等于 背包体积 V 这时我们也不用再去考虑它的子节点因为已经装不下了,我们只需把 ans[i][V] 赋为该物品价值即可其余赋为零,但并不是必要的。
接下来我们来考虑非叶节点的 ans[i][j] 怎么求,我们知道 i 是必选的,那么剩余的就是找到从0到V减去当前体积的各个体积下的最优解,然后把它们在加上 i 节点的价值就是最优解了,但是我们要注意从0到当前体积这部分要赋为0因为这部分体积是装不下 i 的自然就无法再去装 i 的子节点所以价值为0。接下来我们考虑从0到V减去当前体积的各个体积下的最优解的求解思路。那么这部分我们可以换个思路想,现在是有一个体积和几个子节点,同时这些子节点中各个体积都对应着一个价值,我们要在各个子节点以下选择合适的体积,来组合出我们这个体积下的最优解。
这样一描述这个背包问题便又和之前的分组背包问题统一了起来,这每一个子节点就等于分组背包中的一个组别,然后里面不同的体积以及他们对应的价值就等同于分组背包那一个组中不同的物品,我们至多选一个。这样一来我们的问题就有解决的办法了。我们知道分组背包我们是遍历了各个物品来去找最优解,这里我们需要去遍历各个体积了,因为这里不同的体积就对应着不同的物品 所以状态转移方程也就有了,即 ans[i][j] = max(ans[i][j],ans[i][j-k]+ans[son][k]) 其中 i 就是当前节点 j 就是我们当前的体积,son即i 的一个子节点 k 就是我们要遍历的子节点的各个体积不能大于 j 对应着分组背包中一个组别下的各个物品。
C++代码实现:
#include<iostream>
#include<vector>
using namespace std;
struct Thing//定义一个物品结构体包括物品属性体积和价值
{
int vi;
int wi;
};
/*定义以下全局变量*/
vector< vector<int> >tree;//定义一个二维数组存储各个节点的子节点信息
vector<Thing>things; //定义物品数组存储物品的信息
int N,V;//定义物品的数量和背包的体积
int ans[110][110]; //定义二维数组存储答案ans[i][j]表示在选节点i且体积不超过j的前提下所能得到的最大价值
void dfs(int Node)
{/*此函数的功能是在得到一个节点后我们要把在选这个节点的情况下
的每一体积下的最大值都求出来并存入ans数组中 */
if(things[Node].vi <= V)//先判断背包能否放下该节点代表的物品如果可以则进入判断
{
if(tree[Node].size()==0||things[Node].vi == V)
{/*判断如果该节点没有子节点或其代表的物品体积恰好等于背包体积
在这两种情况下都不必再去考虑该节点的子节点怎么选择直接赋值*/
for(int i=things[Node].vi;i<=V;i++)
{/*将大于或等于该物品的各个体积全赋为该物品的价值
并将其他体积全赋值为0这里因为ans为全局变量所以
各点本身就以赋初值为0在这里就不再进行这一操作*/
ans[Node][i]=things[Node].wi;
}
}
else//否则我们需要考虑其子节点的选择情况
{
for(int k=0;k<tree[Node].size();k++)//我们要遍历各个子节点
{
dfs(tree[Node][k]);//首先我们求出选这个子节点的情况下各体积下的最优解
for(int i=V-things[Node].vi;i>=0;i--)
{/*我们开始求选当前节点的情况下各体积下的最优解
我们的体积从V-things[Node].vi开始到0因为我们
确定了选当前物品所以要为他留出空间,体积从大
到小是因为和01背包一样子节点的每种体积至多可以选一遍*/
for(int j=0;j<=i;j++)//当前0到i这个范围就是这个子节点以下所能占用的体积我们要从这些体积中选出最优的那个体积
ans[Node][i] = max(ans[Node][i],ans[Node][i-j]+ans[tree[Node][k]][j]);
/*状态转移我们要用在不选、以及分别从这个子节点以下选总体积为1、2……i的物品中
选择一个最佳决策,即比较当前价值和选不同体积下的价值分别作比较*/
}
}
for(int i=V;i>=things[Node].vi;i--)//最后我们要将我们的根节点的价值加入即开始我们为之空出的体积
{//在这个体积以上的全部加上这个节点代表的物品的价值
ans[Node][i] = ans[Node][i-things[Node].vi] + things[Node].wi;
}
for(int i = 0;i < things[Node].vi;i++ )
{/*这个体积以下的代表这个节点不能选这个节点不能选那么他所有的子节点也不能选所以全部赋初值为0
这里需要赋值是因为我们在上面的求大体积最优解的时候用到了这块体积我们已经为他们赋上值了
它们已经不是0了所以这里的赋0不能省*/
ans[Node][i]=0;
}
}
}
}
int main()
{
int root; //定义一个变量一会储存根节点
cin>>N>>V; //输入物品数和背包体积
tree.resize(N);//开辟空间一个数组储存各节点的子节点信息从而储存整棵树的结点关系
for(int i=0;i<N;i++)//遍历输入物品信息
{
int v,w,f;
cin>>v>>w>>f;
things.push_back({v,w});//将物品信息放入物品数组things
if(f==-1)//检查该节点的父节点是哪一个如果它是父节点则记录它
{
root=i;
}
else//如果不是父节点在在其父节点代表的一维数组中存入该节点
{
tree[f-1].push_back(i);//我们的节点下标从0到n-1输入的父节点是1到n所以让输入的f减去1即我们要找的父节点
}
}
dfs(root);//从根节点调用递归函数
cout<<ans[root][V]<<endl;//输出最终结果
return 0;
}