有依赖背包特点: 有主件,有附件,每种物品只有一件
设 主件的重量main_w[N] 价值main_c[N],附件的重量sec_w[N][N],价值sec_c[N][N]
那么01背包是不是可以看作特殊的有依赖背包,全是主件,没有附件的有依赖背包
01背包的状态转移方程
if (j >= w[i]) dp[j] = max(dp[j], dp[j - w[i]] + c[i])
是不是就可以看成只选主件的有依赖背包的状态转移方程
if (j >= main_w[i]) dp[j] = max(dp[j], dp[j -main_w[i]] + main_c[i])
假设有两个附件需要选择,那么选附件的情况就有三种
选附件1
sec_w[i][1] 的 i代表第i个主件,1代表第一个附件
就把main_w[i]+sec_w[i][i]看作一个重量,main_c[i]+sec_c[i][1]看作一个价值
将只选主件中的main_w[i]和main_c[i]替换就得到了选附件1的状态转移方程
if (j >= (main_w[i]+sec_w[i][1])) dp[j] = max(dp[j], dp[j - main_w[i]-sec_w[i][1]] + main_c[i]+sec_c[i][1]);
下面选附件2和附件1,2同理,不多阐述直接上状态转移方程
选附件2
if (j >= (main_w[i] + sec_w[i][2])) dp[j] = max(dp[j], dp[j - main_w[i] - sec_w[i][2]] + main_c[i] + sec_c[i][2]);
选附件1和2
if (j >= (main_w[i] + sec_w[i][1]+ sec_w[i][2])) dp[j] = max(dp[j], dp[j - main_w[i] - sec_w[i][1]- sec_w[i][2]] + main_c[i] + sec_c[i][1]+ sec_c[i][2]);
下面用一道例题来验证我们的状态转移方程是否正确
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 n 元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:
主件 | 附件 |
---|---|
电脑 | 打印机,扫描仪 |
书柜 | 图书 |
书桌 | 台灯,文具 |
工作椅 | 无 |
如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有 0 个、1 个或 2 个附件。每个附件对应一个主件,附件不再有从属于自己的附件。金明想买的东西很多,肯定会超过妈妈限定的 n 元。于是,他把每件物品规定了一个重要度,分为 5 等:用整数 1∼5 表示,第 5 等最重要。他还从因特网上查到了每件物品的价格(都是 10 元的整数倍)。他希望在不超过 n 元的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第 j 件物品的价格为 vj,重要度为 wj,共选中了 k 件物品,编号依次为 j1,j2,…,jk,则所求的总和为:
vj1×wj1+vj2×wj2+⋯+vjk×wjk
请你帮助金明设计一个满足要求的购物单。
第一行有两个整数,分别表示总钱数 n 和希望购买的物品个数 m。
第 2 到第 (m+1) 行,每行三个整数,第 (i+1) 行的整数 vi,pi,qi 分别表示第 i 件物品的价格、重要度以及它对应的的主件。如果 qi=0,表示该物品本身是主件。
输出一行一个整数表示答案。
输入 #1复制
1000 5 800 2 0 400 5 1 300 5 1 400 3 0 500 2 0
输出 #1复制
2200
对于全部的测试点,保证 1≤n≤3.2×10^4,1≤m≤60,0≤vi≤10^4,1≤pi≤5,0≤qi≤m,答案不超过 2×10^5。
钱数n可以看作背包容量,m物品数量,vi,pi可以看作重量,价值,到这里和01背包是一样的,与01背包不同的是每件物品都有一个qi来标注这件物品是不是主件,属于哪个主件,我们就需要在数据输入时使用 if 语句来判断是不是主件,是主件就记录到main_w[i]中,是附件就纪录到sec_w[qi][k]中,使用状态转移方程时和01背包一样,注意将遍历前i件物品变成遍历前i件主件,注意使用滚动数组,下面是代码
#include
using namespace std;
/*
01背包就是特殊的有依赖背包
所有物品都是主件,并且每种物品只有一件,要么选,要么不选
有依赖背包特点:
有主件,有附件,每种物品只有一件
不选主件
选主件,不选附件
选主件,选第一件附件
选主件,选第二件附件
选主件,选第一件和第二件附件
在以上五种情况中取最大值
*/
const int N = 1e5 + 10;
int m, n, main_w[N], main_c[N], sec_w[N][3], sec_c[N][3],cnt[N];
int dp[N];//状态:dp[j] 前i件容量不超过j的情况下的最大价值
int main()
{
cin >> m >> n;
for (int i = 1; i <= n; i++)
{
int v, p, q;
cin >> v >> p >> q;
if (q == 0) {//主件
main_w[i] = v;
main_c[i] = v * p;
}
else {//附件,q是对应主件的编号
cnt[q]++;//记录主件q的附件个数
sec_w[q][cnt[q]] = v;
sec_c[q][cnt[q]] = v * p;
}
}
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 1; j--) {
//选主件,不选附件
if (j >= main_w[i]) dp[j] = max(dp[j], dp[j - main_w[i]] + main_c[i]);
//选主件,选第一件附件
if (j >= (main_w[i]+sec_w[i][1])) dp[j] = max(dp[j], dp[j - main_w[i]-sec_w[i][1]] + main_c[i]+sec_c[i][1]);
//选主件,选第二件附件
if (j >= (main_w[i] + sec_w[i][2])) dp[j] = max(dp[j], dp[j - main_w[i] - sec_w[i][2]] + main_c[i] + sec_c[i][2]);
//选主件,选第一件和第二件附件
if (j >= (main_w[i] + sec_w[i][1]+ sec_w[i][2])) dp[j] = max(dp[j], dp[j - main_w[i] - sec_w[i][1]- sec_w[i][2]] + main_c[i] + sec_c[i][1]+ sec_c[i][2]);
}
}
cout << dp[m];
}
看到方案数我们第一时间就很容易想到递推求方案数
以上台阶问题为例,一次上1阶,2阶,3阶的递推公式为a[i]=a[i-1]+a[i-2]+a[i-3]
如果我们将1,2,3 记录到w[N]中,然后通过for循环遍历w[i] 然后 a[i]+=a[i-w[i]];
恭喜你已经掌握了一半的dp求方案数了
这样会出现一个问题在背包中我们经常会出现固定的背包体积,物品只能选一次的情况,就无法从dp[0]开始一直到dp[m],因为会无限累加,所以我们倒着找,从m出发一直到w[i],这样就确保了我们每个数字最多选到一次
下面我们给状态和状态转移方程
状态:dp[j]前i件物品在背包容量不超过j的情况下的最多方案数
状态转移方程:if(j>=w[i]) dp[j]+=dp[j-w[i]]
咱们接下来借一道例题一点一点分析这个状态转移方程
有n个正整数,找出其中和为t(t也是正整数)的可能的组合方式。如:
n=5,5个数分别为1,2,3,4,5,t=5;
那么可能的组合有5=1+4和5=2+3和5=5三种组合方式。
输入的第一行是两个正整数n和t,用空格隔开,其中1≤n≤20,表示正整数的个数,t为要求的和(1≤t≤1000);
接下来的一行是n个正整数,用空格隔开。
和为t的不同的组合方式的数目。
5 5
1 2 3 4 5
3
咱们把题目转化成咱们熟悉的背包模型 将1,2,3,4,5 看作物品i的体积,求凑背包容量t的方案数
然后这是代码,咱们在代码下面一点一点推
#include
using namespace std;
const int N = 1e3 + 10,M=1e3+10;
int w[N];//w-容量,价格
int dp[M];
/*
状态:dp[j]前i件物品在背包容量不超过j的情况下的最多方案数
状态转移方程:if(j>=w[i]) dp[j]+=dp[j-w[i]]
*/
int main()
{
int n,t;
cin >>n>>t;
for (int i = 1; i <= n; i++) {
cin >> w[i];
}
dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = t; j >= w[i]; j--) {
dp[j] += dp[j - w[i]];
}
}
cout << dp[t];
}
dp必须注意边界的处理,当背包容量为0时有一种方案,都不选,所以dp[0]=1。
注意事项:j>=w[i]才有意义 又因为我们是倒着走 所以我们用的是更新前的dp[j-w[i]] 相当于dp[i-1][j-w[i]]
i=1 时
j=5 dp[5]=dp[5]+dp[5-1]=0+dp[4]=0; 表示 1 能凑出5的方案数 明显是0,下面同理
j=4 dp[4]=dp[4]+dp[4-1]=0+dp[3]=0; 表示 1 能凑出4的方案数
j=3 dp[3]=dp[3]+dp[3-1]=0+dp[2]=0; 表示 1 能凑出3的方案数
j=2 dp[2]=dp[2]+dp[2-1]=0+dp[1]=0; 表示 1 能凑出2的方案数
j=1 dp[1]=dp[1]+dp[1-1]=0+dp[0]=1; 表示 1 能凑出1的方案数
i=2 时
j=5 dp[5]=dp[5]+dp[5-2]=0+dp[3]=0; 表示 1,2 能凑出5的方案数
j=4 dp[4]=dp[4]+dp[4-2]=0+dp[2]=0; 表示 1,2 能凑出4的方案数
j=3 dp[3]=dp[3]+dp[3-2]=0+dp[1]=1; 表示 1,2 能凑出3的方案数
j=2 dp[2]=dp[2]+dp[2-2]=0+dp[0]=1; 表示 1,2 能凑出2的方案数
i=3 时
j=5 dp[5]=dp[5]+dp[5-3]=0+dp[2]=1; 表示 1,2,3 能凑出5的方案数
j=4 dp[4]=dp[4]+dp[4-3]=0+dp[1]=1; 表示 1,2,3 能凑出4的方案数
j=3 dp[3]=dp[3]+dp[3-3]=1+dp[0]=2; 表示 1,2,3 能凑出3的方案数
i=4 时
j=5 dp[5]=dp[5]+dp[5-4]=1+dp[1]=2; 表示 1,2,3,4 能凑出5的方案数
j=4 dp[4]=dp[4]+dp[4-4]=1+dp[0]=2; 表示 1,2,3,4 能凑出4的方案数
i=5 时
j=5 dp[5]=dp[5]+dp[5-5]=2+dp[0]=3; 表示 1,2,3,4,5 能凑出5的方案数
下面是完整的dp表
dp[1][5]=0 dp[1][4]=0 dp[1][3]=0 dp[1][2]=0 dp[1][1]=1
dp[2][5]=0 dp[2][4]=0 dp[2][3]=1 dp[2][2]=1
dp[3][5]=1 dp[3][4]=1 dp[3][3]=2
dp[4][5]=2 dp[4][4]=2
dp[5][5]=3