笔记---背包问题

1.01背包

        有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

        第 i 件物品的体积是 v[i],价值是 w[i]。

        求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

对于01背包问题,只有选或不选,所有选法的集合便是由选的情况和不选的情况组成的

以下为未优化二维做法

#include<iostream>
using namespace std;
const int N = 1e3 + 10;
int n, m;
int w[N], v[N];
int f[N][N];	//f[i][j]代表【从前i个物品中选,且最大容量为j的要求下的最大价值】
int main() {

	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> v[i] >> w[i];
	}

	for (int i = 1; i <= n; i++) {			//枚举n个物品
		for (int j = 1; j <= m; j++) {		//枚举m个体积
			if(j < v[i])f[i][j] = f[i - 1][j];//如果j的体积甚至不如第i个物品大
											  //那么也没有决策的必要了,当前的最优解就是上一层的最优解
			if (j >= v[i]) f[i][j] = max(f[i-1][j], f[i - 1][j - v[i]] + w[i]);	
            //如果体积可以装的下第i个物品,那么要进行决策
			/*
			首先前者f[i-1][j]代表状态不变,即不选择第i个物品时的{最优解}
			f[i-1][j-v[i]]+w[i]代表的是,选择上第i个物品时的{最优解},采用先去掉一个i,再加上一个i的价值的算法
			取max则是从二者中抉择出最优的{最优解}
			*/

			//此处不能暴力的直接每一层都取最大价值(使用贪心算法),贪心算法只注重于当前的利益
			//无法保证背包的容量被充分利用,从而无法保证最终得到的结果是最优解
			//动态规划便是对全局的决策,得到最优解
		}
	}

	cout << f[n][m];
	return 0;
}

一下为优化后的一维做法

 f[i][j]可以变为f[j]?    题目仅要求最终的f[n][m]结果,故讨论选前多少个物品意义不大

如果在原代码上进行修改可以得到

if(j <v[i]) f[i][j] = f[i-1][j];
                ||
                \/
if(j <v[i])     f[j] = f[j];
//为等式,故可以直接删除

所以可以直接从v[i]枚举到m

但对于j >= v[i]时,在二维做法上,每次第i层的取max都用到了第i-1层,即在二维枚举时,保持了f[i-1]在使用时是原始数据,是没有被更新过的,没有被污染的

如枚举一个体积为1的物品时 f[2][3] = max(f[1][3] , f[1][2] + w[2]); 这时的f[1][2],f[1][3]都是没有被更新过的

而如果一维枚举时,如果直接使用升序枚举,那么就会导致使用 f[j] 时 f[j] 已经是被污染过的

如枚举一个体积为1的物品时 f[3] = max(f[3] , f[2] + w[2]); 那么此时f[2]已经在之前的枚举时被更新过了,导致了现在进行决策时f[2]已经不是原始数据了

综上,一维列举时需要逆序进行,这样可以避免小体积被提前更新

#include<iostream>
using namespace std;
const int N = 1e3 + 10;

int f[N];
int n, m;
int v[N], w[N];

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> v[i] >> w[i];
	}

	for (int i = 1; i <= n; i++) {
		for (int j = m; j >= v[i]; j--) {
			f[j] = max(f[j], f[j - v[i]] + w[i]);
		}
	}

	cout << f[m];
	return 0;
}

2.完全背包

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是 v[i],价值是 w[i] 。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

完全背包问题中的物品是没有选择多少的限制的,故我们可以把集合的组成划分成:选1,2,3...k个前i个物品。

用代码实现出来便是这样

#include<iostream>
using namespace std;
const int N = 1e3 + 10;

int n, m;
int v[N], w[N];
int f[N][N];	//f[i][j]表示从前i种物品中选,体积不超过j的选法的最优解

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> v[i] >> w[i];
	}

	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= m; j++) {
			for (int k = 0; k * v[i] <= j; k++) {	//枚举选择第i种物品的个数	
				//下面的转移方程可能难以理解,但其实真正写出来是:
				//f[i][j] = max(f[i-1][j-v[i]*k] +w[i]*k)  (k = 0,1,2...)
				f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
			}
		}
	}

	cout << f[n][m];
	return 0;
}

那么此问题如何优化? 

     f[i][j] = max(f[i-1][j] , f[i-1][j-v[i]] + w[i], f[i-1][j-2v[i]] + 2w[i] , f[i-1][j-3v[i]] + 3w[i], ......)

f[i][j-v[i]] = max(f[i-1][j-v[i]] , f[i-1][j-2v[i]]+w[i], f[i-1][j-3v[i]] +2w[i], ......)

 通过观察上面两式,可以得知f[i][j-v[i]]的每一项都比上面的每一项少了一个w[i],故可得

f[i][j] = max(f[i-1][j] , f[i][j-v[i]] + w[i])

根据如上推导,可得到新的优化后的代码:

#include<iostream>
using namespace std;
const int N = 1e3 + 10;

int n, m;
int v[N], w[N];
int f[N][N];	//f[i][j]表示从前i种物品中选,体积不超过j

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> v[i] >> w[i];
	}

	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= m; j++) {

			if(j < v[i]) f[i][j] = f[i - 1][j];		//体积不够时,不决策
			if (j >= v[i]) f[i][j] = max(f[i-1][j], f[i][j - v[i]] + w[i]);	//体积足够时,进行决策,根据推得的状态转移方程

		}
	}

	cout << f[n][m];
	return 0;
}

这么看,是不是和01背包的方程十分相似?

完全背包也可以继续优化成一维,但是与01背包不同的是,枚举体积时不需要变为逆序

回顾如上状态转移方程,都是由第i层转移来的,而不是i-1层,也就是说其正序枚举也是和优化前的状态转移方程式相同的

优化后代码如下:

#include<iostream>
using namespace std;
const int N = 1e3 + 10;

int n, m;
int v[N], w[N];
int f[N];	

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> v[i] >> w[i];
	}

	for (int i = 1; i <= n; i++) {
		for (int j = v[i]; j <= m; j++) {
			f[j] = max(f[j], f[j - v[i]] + w[i]);
		}
	}

	cout << f[m];
	return 0;
}

当然还可以让价值和体积不用数组存储,而是分散在每一步进行输入,此处不再给出 

3.多重背包 

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 s[i] 件,每件体积是 v[i],价值是 w[i]。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

道理与完全背包相同,仅仅是物品的数量有了限制

则集合的划分方式仍然是划分为选0,1,2,.....k个第i个物品

则以 f[i][j] 表示从前i件物品中选,且体积不超过j的最大价值

可得到                  f[i][j] = max(f[i-1][j-v[i]*k] +w[i]*k)  (k = 0,1,2,3......s[i])

可得朴素代码为:
 

#include<iostream>
using namespace std;
const int N = 100 + 10;

int n, m;
int v[N], w[N], s[N];	//体积,价值,数量
int f[N][N];

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> v[i] >> w[i] >> s[i];

	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			for (int k = 0; k <= s[i] && k*v[i] <= j; k++) {	//k不超过数量且k个i物品的体积不超过当前体积
				f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);	//状态转移方程
			}
		}
	}

	cout << f[n][m];
	return 0;
}

如何优化?可以再次列出公式

f[i][j] = max(f[i-1][j],f[i-1][j-v[i]]+w[i],f[i-1][j-2vi]]+2w[i] , ...... f[i-1][j-s[i]*v[i]]+s[i]*w[i]);

f[i-1][j-v[i]] = max(f[i-1][j-v[i]],f[i-1][j-2v[i]]+w[i],f[i-1][j-3v[i]] + 2w[i],.....f[i-1][j-s[i]*v[i]]+(s[i]-1)*w[i] + f[i-1][f-s[i]*v[i]-v[i]]+s[i]*w[i]);

此次同样可以发现f[i-1][j-v[i]]比f[i][j]每项多了w[i],但是f[i-1][j-v[i]]比f[i][j]多了一项(为什么多了这一项?因为[j-v[i]]仅仅代表了这个状态的体积,而第i个物品仍然是具有s[i]项的)而每次取max总不能用减法来取max吧,所以我们需另求他法

那么究竟如何优化呢:使用倍增的思想

举个例子:当第i个物品的数量,即s[i],为1234时

我们将其打包为:

1, 2, 4, 8, 16, 32, ......,512, 240

对应为

2^0, 2^1, 2^2, 2^3, 2^4, 2^5,......2^9, (s[i] - \sum 2^n)

也就是说使用2的n次方来打包,在如果再增加一个2^n时,所有打包的加和已经超过了本来的总数s[i]的时候,使用s[i]减去所有打包的数量的集合,得到最终最后一个打包的数量

之后我们把每个打包好的看作是一些01背包的物品,也就是说对于这个打包好的物品我们只有选或不选的可能,之后对其进行枚举,可以得到对s[i]枚举的所有可能(不再证明)

由此我们得到优化后的代码:

#include<iostream>
using namespace std;
const int N = 3 * 1e5 + 10;

int f[N];
int v[N] = { 0 };
int w[N] = { 0 };		//采用此优化方法不再开辟s[i]
int n, m;

int main() {
	
	int j = 0;			//对每个包的指针
	cin >> n >> m;		//读入种数和体积

	for (int i = 0; i < n; i++) {	//枚举每一种物品

		int a, b, s;		//a:体积,b:价值,s:数量
		cin >> a >> b >> s;

		int packages = 1;	//packages:每个包存储的数量,从1开始

		while (packages <= s) {	//当s还可以被分开打包时
			j++;				//指针先++

			v[j] = a * packages;//第j个包的体积便为包中物品的数量乘上每个物品的体积
			w[j] = b * packages;//第j个包的价值便为包中物品的数量乘上每个物品的价值

			s -= packages;		//s先减去当前包中物品的数量
			packages *= 2;		//包中数量变为2倍
		}
		if (s > 0) {			//如果不可以被2的n次方瓜分
			j++;				//再进行最后一次存储,即剩下的所有物品分为一个包
			v[j] = a*s;
			w[j] = b * s;
		}
	}

	n = j;						//看作是01背包的物品的数量就是j所指向的最后一个下标
	
	//以下套用了01背包的方法
	for (int i = 1; i <= n; i++) {	
		for (int k = m; k >= v[i]; k--) {
			f[k] = max(f[k], f[k - v[i]] + w[i]);
		}
	}

	cout << f[m];
	return 0;
}

4.分组背包 

有 N 组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 v[i][j],价值是 w[i][j],其中 i 是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

f[i][j]表示从前i组里选,体积不超过j的最大价值

在枚举时,枚举第i组物品选哪一个或者不选

#include<iostream>
using namespace std;
const int N = 100 + 10;

int v[N][N], w[N][N], s[N];
int f[N];
int n, m;

int main() {
	cin >> n >> m;

	for (int i = 1; i <= n; i++) {
		cin >> s[i];
		for (int j = 1; j <= s[i]; j++) {
			cin >> v[i][j] >> w[i][j];
		}
	}

	for (int i = 1; i <= n; i++) {
		for (int j = m; j >= 1; j--) {			//需要用到i-1层的状态,则逆序进行
			for (int k = 1; k <= s[i]; k++) {	//枚举每一件物品
				if (v[i][k] <= j) {				//如果这件物品的体积没有超过限制的体积
					f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);	//选或不选决策
				}
			}
		}
	}

	cout << f[m];
	return 0;
}

  • 11
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值