算法心经——01背包问题

01背包问题

引例:
现有一个麻袋,所能装下的最大质量为M,现有N件物品,每件物品既有价值又有质量,他们的价值和质量有可能不完全相同,现问:如何安排才能使麻袋一次装下物品的价值最大?

原理

这就是我们今天要讨论的01背包的问题。乍一看,好像是个贪心思路,没错,但本题有个限制:背包容量。所以不能使用传统的贪心算法,那么退一步,能不能把背包容量从1到M分成M类,再把商品从1到N编号,两两组合,依次讨论,在每一类里用贪心使价值最大,逐层往上,不就可以找到答案了吗?
比如:
1. 我可以先从背包容量为1 (第一层)开始,分别与这N个商品两两组合,对每一种组合我要考虑,对于该件商品,我是拿还是不拿?如果拿(前提背包还能装得下),我的价值就是背包为0时最大的价值(背包为0什么都装不了,总价值自然为0)+这件商品的价值;如果不拿就相当于什么都没做,总价值不变还是原来背包为0时最大的价值。两种情况下选一个最大的作为本层,也就是背包容量为1时所能达到的最大价值。

2. 然后到了第二层(背包容量为2),同样对N件商品两两组合,考虑拿还是不拿?如果拿,那我的总价值就是背包为1时最大的价值(我们在第一层已经求出来了)+这件商品的价值;如果不拿,那就还是原来背包为1时最大的价值。两种情况下选一个最大的作为本层,即背包容量为1时所能达到的最大价值。

3. 然后就到了第三层(背包容量为3),同样对N件商品两两组合,考虑拿还是不拿?如果拿,那我的总价值就是背包为2时最大的价值(我们在第二层已经求出来了)+这件商品的价值;如果不拿,那就还是原来背包为2时最大的价值。两种情况下选一个最大的作为本层也就是背包容量为3时所能达到的最大价值。
……
如此找下去,直到第M层,我们发现每一层都需要依赖前一层的结果进行讨论,所以这是一个动态规划的思想。 所以我们可以得出结论,所谓01背包问题就是贪心思想 + 动态规划。

下面我们就上面所做的讨论列出表格,为了方便讲解,我们令引例中的M = 8,N = 4,每件商品的信息如下表所示
物品编号1234
w体积2345
v价值3456
首先需要初始化条件,即对背包容量为0及商品为0的情况做出统计,显然最大价值都是0,其中每一行表示,第i件物品对应0 1 2... j背包容量时所能达到的最大价值。

在这里插入图片描述
根据上面的讨论,一层层得出结果,每行列的交点表示对于第i件商品,背包容量为j时,所能达到的最大价值。
在这里插入图片描述
设背包数组为dp[i][j],正如前面所讲,其内容表示对于第i件商品,背包容量为j时所能达到的最大价值,于是我们可以得出递推公式。

d p [ i ] [ j ] { d p [ i − 1 ] [ j ] , 第 i 件 太 重 , 装 不 下 m a x { d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] , 拿 d p [ i − 1 ] [ j ] , 不 拿 dp[i][j]\left\{\begin{matrix} dp[i-1][j], 第i件太重,装不下\\ max \left\{\begin{matrix} dp[i-1][j-w[i]] + v[i],拿 \\ dp[i-1][j],不拿 \end{matrix}\right. \end{matrix}\right. dp[i][j]dp[i1][j],imax{dp[i1][jw[i]]+v[i],dp[i1][j],


代码实现

下面我们给出代码

#include<iostream>
using namespace std;
int w[5] = { 0,2,3,4,5 };
int v[5] = { 0,3,4,5,6 };
int N = 5, W = 9;
int dp[5][9] = { 0 };//N件商品,背包容量为W
void knapsack() {
	for (int i = 1; i < N; i++) {//商品序号
		for (int j = 1; j < W; j++) {//背包容量
									 //第k件太重
			if (w[i] > j) {
				dp[i][j] = dp[i - 1][j];
			}
			else {
				int v1 = dp[i - 1][j - w[i]] + v[i];
				int v2 = dp[i - 1][j];
				if (v1 > v2) {
					dp[i][j] = v1;
				}
				else {
					dp[i][j] = v2;
				}
			}
		}
	}
}
int main() {
	knapsack();
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < W; j++) {
			cout << dp[i][j] << " ";
		}
		cout << endl;
	}
	return 0;
}

路径回溯

下面请大家想一想,如何找到“背包路径”呢?即达到最大价值时,我的背包里都装了哪些商品。其实如果对前面所讲的内容理解到位了,回答这个问题应该不难。如果回答不上,建议从头再理解一遍。
只有当dp[i][j] == dp[i-1][j-w[i]] + v[i]的时候才说明第i件物品被选中了,从j = M到j = 1按这个关系式层层筛选就可以找到路径。
比如:
最优解为V(4,8)=10,而V(4,8)!=V(3,8)却有V(4,8)=V(3,8-w(4))+v(4)=V(3,3)+6=4+6=10,所以第4件商品被选中,并且回到V(3,8-w(4))=V(3,3);
有V(3,3)=V(2,3)=4,所以第3件商品没被选择,回到V(2,3);
而V(2,3)!=V(1,3)却有V(2,3)=V(1,3-w(2))+v(2)=V(1,0)+4=0+4=4,所以第2件商品被选中,并且回到V(1,3-w(2))=V(1,0);
有V(1,0)=V(0,0)=0,所以第1件商品没被选择。
int i = n, j = m;
while (j > 0){
 if (dp[i][j] == dp[i-1][j-w[i]]+v[i]) {//选中
	 printf("%d", v[i]);
	 j -= w[i];
	 i--;
	 if (j != 0)printf(" ");
 }
  else {
	 i--;//没有选中该商品,检查上一个商品
   }
}

值得注意的是,该循环执行完毕只能得到一组解,因为给定的i是从n开始的,要想找到所有解,需再套一层,找到满足最大值的所有i的集合。

……THE END……
数据及部分图表来自https://blog.csdn.net/qq_38410730/article/details/81667885
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值