DP学习笔记(7):有依赖背包,背包求方案数

有依赖背包

常规分析

有依赖背包特点: 有主件,有附件,每种物品只有一件

设 主件的重量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]);

下面用一道例题来验证我们的状态转移方程是否正确 

例题:洛谷——P1064 [NOIP 2006 提高组] 金明的预算方案

题目描述

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 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]]

咱们接下来借一道例题一点一点分析这个状态转移方程

例题:奥赛一本通——1291:数字组合

【题目描述】

有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

你可能感兴趣的:(学习,笔记,算法)