转自:https://www.cnblogs.com/fengziwei/p/7750849.html
感觉自己01背包很弱,所以学习下01背包问题,并写下自己的感悟
下列所有代码的输入:
Input
2 2 6 5 4
6 3 5 4 6
//背包承重: 10 第一行为wight 第二行为 value 一共5个物品
Output
01背包: 15
完全背包:30
//首先暴搜肯定能做,不谈
1.背包问题
0-1背包问题是指每一种物品都只有一件,可以选择放或者不放。现在假设有n件物品,背包承重为m。
对于这种问题,我们可以采用一个二维数组去解决:f[n][m],其中n代表加入背包的是前n件物品,m表示背包的承重,f[n][m]表示当前状态下能放进背包里面的物品的最大总价值(在n个物品中,m背包承重能装的最大总价值)。f[n][m]就是我们的最终结果。
采用动态规划,必须要知道初始状态和状态转移方程。初始状态很容易就能知道,那么状态转移方程如何求呢?对于一件物品,我们有放进或者不放进背包两种选择:
我们的状态转移方程就是:f[n][m] = max( f[n - 1][m] , f[n - 1][m - weight[n]] + value[n])
//f[n - 1][m]代表第n个物品不放 f[n - 1][m - weight[n]] + value[n]代表第n个物品放进去
(1)假如(第n个物品)我们放进背包,那么就是 f[n - 1][m - weight[i]] + value[i],这里的f[n - 1][m - weight[i]] + value[i]应该这么理解:本来m承重背包在n个物品中选择最大值,因为第n个物品放入,所以我们现在考虑的是用m-weight[n](第n个物品放进去容量减少第n个物品的承重)从剩下的n-1个物品中选择出最大价值。有而对于f[i - 1][j - weight[i]]这部分,i - 1很容易理解,关键是 j - weight[i]这里,我们要明白:要把这件物品放进背包,就得在背包里面预留这一部分空间。
(2)假如我们不放进背包,f[n][m] = f[n - 1][m],这个很容易理解。//本来从n个中选择,但是第n个不放进去就变成从n-1个中选择
还有一种特殊的情况f[n][m] = f[n - 1][m],就是背包容量放不下第n个物品。//需要注意当n=1时候m
下面是我写的代码:
//注:【F[n][m]的含义是容量m的背包从n个物品中选择出价值最大的】
//非常重要的一点我们在考虑从n个物品选择m重量的最大价值,是从第n个物品开始考虑,而不是从第一个物品开始考虑
//先考虑第n个能不能放,如果能放,就考虑第n个物品放不放,放不放取决于n-1个物品中的最大值,n-1又由n-2决定
//因此规模不断的缩小,最终达到 第1个物品 mi 重量放不放,mi<第1个物品重量是就是0,啥也装不下
//因此其实 F[1][0]---到----F[1][W[1]-1] 结果都是0 F[1][W[1]]---到---F[1][m] 结果都是 V[1]
//说一个很直白的列子(递归比较好理解):假设有5个物品,重量为12345,价值也是12345,我有12的背包,我想求的答案是F[5][12] (= = 这个值是多少我不知道),第5个物品重量为5,价值也为5,我有12的背包显然能放下,所以我现在考虑的是为了价值最大,我究竟放不放第5个物品,这个取决于F[4][7] ( 第5个放 ) 和F[4][12] ( 第5个不放 ) 的值谁大,这两个值我也不知道多少,好吧继续递归,F[4][7]的最大值又取决于第4个物品放不放,好吧和第5个物品一样了,变成了取决于F[3][3]和F[3][7]的值.....规模是不是在减少....最终会到底 F[1][0]---到----F[1][wight[1]-1] 结果都是0 F[1][wight[1]]---到---F[1][m] 结果都是 V[1]
递归想法就和求斐波那契一样 F(n)=F(n-1)+F(n-2)//Fn不知道Fn-1 Fn-2也不知道,但是F1知道F2也知道(为了速度求出的Fn用表保存下来)
for循环呢就是先求F1,F2.....直到Fn,在此题背包中呢,就是F[1][0]-----F[1][m],然后求F[2][0]---F[2][m]
# include
# include
# define num 5
# define wight 10
using namespace std;
int F_[6][20];
int V[5],W[5];
int F[6][20];
int f01(int n, int m) {
if (F_[n][m]!=-1)return F_[n][m];//如果之前递归已经求出该值就返回
if (m < W[n - 1])return F_[n][m] = ((n == 1) ? 0 : f01(n - 1, m));//此处考虑能不能放进去
else//状态转移方程: f [n][m]=max{f[n-1][m],f[n-1][m-w[i]]+v[i]}
return F_[n][m]=max(f01(n - 1, m), f01(n - 1, m - W[n - 1]) + V[n - 1]);//此时能够放下,考虑的是放不放
}//注:【F[n][m]的含义是容量m的背包从n个物品中选择出价值最大的】
//非常重要的一点我们在考虑从n个物品选择m重量的最大价值,是从第n个物品开始考虑,而不是从第一个物品开始考虑
//先考虑第n个能不能放,如果能放,就考虑第n个物品放不放,此时是看n-1个物品中的最大值,n-1又由n-2决定
//因此规模不断的缩小,最终达到 第1个物品 mi 重量放不放,mi<第1个物品重量是就是0,啥也装不下
//因此其实 F[1]---到----F[1][W[1]-1] 结果都是0 F[1][W[1]]---到---F[1][m] 结果都是 V[1]
int main(void) {
memset(F_, -1, sizeof(F_));
for (int i = 0; i < 5; i++)cin >> W[i];
for (int i = 0; i < 5; i++)cin >> V[i];
cout << "递归max:" << f01(num, wight) << endl;
for (int i = 1,j; i <=num; i++) {
for (j = 0; j <= wight; j++) {
if (j < W[i - 1]) {//第i个东西放不下
F[i][j] = (i == 1 ? 0 : F[i - 1][j]);
}
else {//放的下 考虑的是放不放
F[i][j] = max(F[i - 1][j], F[i - 1][j - W[i - 1]] + V[i - 1]);
}
}
}
cout << "循环max:" << F[num][wight] << endl;
system("pause");
return 0;
}
0-1背包问题还有一种更加节省空间的方法,那就是采用一维数组去解决,下面是代码:
# include
# include
# define num 5
# define wight 10
using namespace std;
int V[num+1], W[num+1];
int F[wight+1];
int main(void) {
memset(F, 0, sizeof(F));
for (int i = 1; i <= num; i++)cin >> W[i];
for (int i = 1; i <= num; i++)cin >> V[i];
//01背包
for (int i = 1, j; i <= num; i++) {
// for (j = 0; j <= wight; j++) {//正序的话其实是完全背包的解法,不需要考虑物品个数
for (j = wight; j >= 0; j--) {//此处一定要倒序
if (j >= W[i]) {//能够放下的时候考虑第i个物品放不放
F[j] = max(V[i] + F[j - W[i]], F[j]);
}
else
F[j] = F[j] ? F[j] : 0;//放不下就是原来的值
}
}
cout << "循环max:" << F[wight] << endl;
system("pause");
return 0;
}
首先肯定很多人疑惑,为啥第二个循环是倒序......其实我一开始也疑惑,我认为正序完全可以做,后来尝试发现不可以,必须要倒序,所以这里说下我个人对于倒序的理解。重要的一点说明下:对于代码中的F[m]的含义为:m承重的包所能装下的最大价值。开始我想,从0--m承重开始循环,如果当前的物品(当前物品取决于外循环的 i 如果 i=5,那么就从第5个物品开始考虑)能放进去,那么当前的质量就会变成m-wight[i] ,并且m-wight[i] < m,在之前的操作,F[m-wight[i]]的最大值一定是已知的。显然这个F[m]的选择取决于F[m]=max(F[m-wight]+V[i],F[m]) ,至于为啥和F[m]比较很简单,之前的F[m]在物品i-1个中已经求解了没有(不放)第 i 件物品的最大值,那么现在只要考虑放进去后的值是否大于不放进去的值即可,其实就是比较第i个物品放不放进去。这么一看确实逻辑上没有一点问题。但是,但是,但是,最要的东西说三遍! 如果正序承重从0-m,会重复多放物品,导致数据错误。
举个例子吧:下图为例,i=1 为e物品,i=5 为a物品 ( 顺序别错了 )从下往上看
当i=1,考虑第一个物品e的时候,承重m 1,2,3都没问题,放不进去,总价值为F[1,2,3]=0,到了4的时候刚好能放进去,总价值为6,也没问题,7也没有问题,到了8会出现一个问题,这时候发现第i个物品(就是例子的第1个物品)又可以放进去,还会剩余4个承重,F[4]=6之前算过,就会出现这种情况 F[8]=max(F[8-4]+V[1],F[8]),导致F[8]=12,第1个物品被放进了2次。 【后来我才发现其实这个是完全背包的一维数组解法】(纯属意外惊喜)
如果倒过来就不会出现这种情况,很简单,表在初始的时候被 memset(F,0,sizeof(F)) , F表的所有数据为0,还是用第1个物品举例子(外循环 i=1),10承重可以放进去F[10]=max(F[10-4]+V[1],F[10]) ,其中F[6]=0因为没有求解呢....初始化为0,所以F[10]=6,因此不会出现重复放置的问题,到了第2个物品的时候(i=2),F[10]=max(F[10-5]+V[2],F[10]),其中F[5]在第1个物品的时候计算为6(F[5]=max(F[5-4],F[5])),所以F[10]就被重新计算成了10,后面就一样了,相同的方式。
2.完全背包问题
完全背包问题是指每种物品都有无限件
解法就是刚才的一维数组的解法,只不过内循环用正序。
for (int i = 1, j; i <= num; i++) {
for (j = 0; j <= wight; j++) {
if (j >= W[i]) {//能够放下的时候考虑第i个物品放不放
F[j] = max(V[i] + F[j - W[i]], F[j]);
}
else
F[j] = F[j] ? F[j] : 0;//放不下就是原来的值
}
}
为啥逆序就变成了完全背包的解法,这个其实还是很好理解的,因为在正序的基础上,我们其实不用考虑第i个物品的个数,我们只需要不停的考虑它能不能放进去,如果能放进,剩余的空间还能够放下的最大值和第i个物品的价格能否大于当前F[m]的价值。
客官还是好好想想....
代码也可以优化成:不在从0---到---m 而是从wight[i]---到---m
for (int i = 1; i <= n; i++) {
for (int j = weight[i]; j <= m; j++) {
f[j] = max(f[j], f[j - weight[i]] + value[i]);
}
}
这个也很好理解 承重 j 如果小于weight[i],那么说明第 i 个物品放不下,F[j]的值不会变化还是i-1时候求解的F[j]
3.多重背包问题
多重背包意思就是:物品有个数,重量,价值。数量不在是1个或者无数个
说实话,其实没必要说那么多,为啥,因为很简单,当你看懂01背包的那一刻你已经就能求解多重背包和完全背包了,为啥,因为完全背包可以转化为多重背包,多重背包可以转化为01背包,至于为啥要说那是为了专精一种问题的求解。至于为啥可以转化,也很简单,完全背包虽然说物品的个数有无数个,但是由于承重m已经确定,每个物品又有重量,导致其实单一装1种物品的时候,每个物品能装的最大值已经出来了,至于更多的数量也没有意义,因为装不下。这时候由于每种物品都有了数量,导致完全背包变成了多重背包。多重背包转化01也很简单,物品1有2个,价值为4,我们可以转化为 a ,b物品,价值都是4。然后就变成了01背包问题了。但是我们知道多重背包如果一个一个转化为01背包还是非常慢的,这里我最近又学习了一种方法,所以特地加进了文章,那就是多重背包转化01背包利用二进制优化。
二进制优化:
大家知道任意一个十进制数都可以转换成二进制,那么假设某种物品有1023种,即2^10-1,二进制为111111111,则可以视为每一位分别是一个由{1,2,4,8,16,32,64,128,256,512}个物品糅合成的大物品,二进制数每一位0表示不取,1表示取,这样我们就可以取遍所有的数,而若不是2的整数幂-1个物品,再补上缺的物品个数就好了,比如1025,就再补上个由2个物品组成的新物品。
例如把22进行二进制拆分:
成为1,2,4,8,7;由1,2,4,8可以组成1--15之间所有的数,而对于16--22之间的数,可以先减去剩余的7,那就是1--15之间的数可以用1,2,4,8表示了。
多重背包的代码:
for (int i = 1, j; i <= num; i++) {
for (int k = 0; k < num[i]; k++) {//加上个数当做别的物品只不过价值和重量和i物品一样
for (j = wight; j >= 0; j--) {//倒序求解01背包
if (j >= W[i]) {//能够放下的时候考虑第i个物品放不放
F[j] = max(V[i] + F[j - W[i]], F[j]);
}
else
F[j] = F[j] ? F[j] : 0;//放不下就是原来的值
}
}
}
多重背包转二进制问题代码:
memset(F, 0, sizeof(F));//F初始化0
for (int i = 0; i < 5; i++)cin >> W[i];//录入单个重量
for (int i = 0; i < 5; i++)cin >> V[i];//录入单个价值
for (int i = 0; i < 5; i++) {//依次录入数量
cin >> nu[i];
for (int j = 1; j <= nu[i]; j <<= 1) {//数量开始二进制拆分
value_[k] = j*V[i];//总价值=个数*单价
wight_[k++] = j*W[i];//总重量=个数*单重
nu[i] -= j;//剩余个数更新
}
if (nu[i]) {//剩余部分存储
value_[k] = nu[i] * V[i];
wight_[k++] = nu[i]*W[i];
}
}//以上步骤将多重背包二进制转化为01背包
//下述为01背包做法
for (int i = 0, j; i < k; i++) {//个数
for (j = wight; j >= 0; j--) {//倒序质量
if (j >= wight_[i]) {//能够放下的时候考虑第i个物品放不放
F[j] = max(value_[i] + F[j - wight_[i]], F[j]);
}
else
F[j] = F[j] ? F[j] : 0;//放不下就是原来的值
}
}
最后补充的一点:以上所有做法的参考量都是重量,我们都是从背包承重开始考虑的,但是其实背包问题也可以从其他方面开始考虑,比如说价格,它的本质状态转移方程思想并没有变化,例如价格依旧:F[value]=max(F[value],F[value-value[i]]+value[i])
不管怎么考虑,顺序还是从第i个物品开始考虑,这东西我能不能拿,能拿了的话,我到底拿不拿?