0/1背包完全详解
1.1 问题描述
有N件物品和一个大小为M的背包,以及若干物品,每种物品只有一件,大小分别为w[i],其价值分别为val[i]。问题:将哪些物品装入背包,可使得背包内的物品价值总和最大?
输入:
第一行:N M
接下来N行,每行两个整数,w[i]和val[i]。
输出:最大价值Vmax.
1.2 解题思路
由于每种物品都只有一件,所以只存在两个选择:装这个物品或者不装。(也就是0/1背包的含义)
我们定义一个数组bag [i] [v]用于储存——当背包大小为v,且选择处理完第i个物品后(前i个物品选择装或不装)的最大价值总和,那么最终的答案就是bag [n] [m]。
我们通过一个例子来推出bag [i] [v]的递推式。
有3个物品,大小价值如下表,和一个大小为3的背包。
物品/背包大小 | 1 | 2 | 3 |
---|---|---|---|
A,大小1,价值2 | |||
B,大小2,价值5 | |||
C,大小1,价值3 |
先考虑装不装A。如下表。
物品/背包大小 | 1 | 2 | 3 |
---|---|---|---|
A,大小1,价值2 | 2 | 2 | 2 |
B,大小2,价值5 | |||
C,大小1,价值3 |
由于A的大小只有1,所以大小1~3的背包都可以装下它,于是更新所有值为A的价值2。
接下来考虑B。如下表。
物品/背包大小 | 1 | 2 | 3 |
---|---|---|---|
A,大小1,价值2 | 2 | 2 | 2 |
B,大小2,价值5 | 2 | 5 | 7 |
C,大小1,价值3 |
由于B的大小为2,所以大小为1的背包装不下它,值不变。
大小为2的背包可装下它,且值为5,比原来的2大,于是更新值为5。
大小为3的背包可装下它,且装完后剩余大小为1。
如果装B,那么价值=B的价值+未装B的、大小为1的背包的最大价值=5+2=7。
如果不装B,那么价值就是原来的值=2。在二者里取max,就能得到最大值,也就是7。
最后考虑C。如下表。
物品/背包大小 | 1 | 2 | 3 |
---|---|---|---|
A,大小1,价值2 | 2 | 2 | 2 |
B,大小2,价值5 | 2 | 5 | 7 |
C,大小1,价值3 | 3 | 5 | 8 |
C的大小为1,背包1可装,值更大,更新为3。
背包2可装,如果装,那么价值=C的价值+未装C的、大小为1的背包的最大价值=3+2=5。
如果不装,值就是原来的5。二者取max即可。
背包3可装,如果装,那么价值=C的价值+未装C的、大小为2的背包的最大价值=5+3=8。
如果不装,值就是原来的7。二者取max即可。
所以最后输出的值就是8。
小结论:
我们可以发现递推公式:bag [i] [v] = max ( bag [i-1] [v] , bag [i-1] [ v - w [i] ] + val [i] )。
也就是处理第i件物品,背包大小为v时的最大价值,有两种可能。
①装入第i件物品
那么价值=该物品的价值+未装入该物品的、大小足够塞入该物品的背包的最大价值=bag [i-1] [ v - w [i] ] + val [i]。
②不装入第i件物品
那么价值就是处理i-1件物品时计算出来的值,不变。
二者取最大值即可。
1.2.1 代码:
#include <stdio.h>
int max(int a,int b)``
{
if(a>b) return a;
else return b;
}
int bag[1000][1000];//储存解的数组
int w[1000],val[1000];//每件物品的大小与价值
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d%d",&w[i],&val[i]);//读入数据
for(int i=1;i<=n;i++) //从第一件物品一直处理到第i件物品
for(int j=1;j<=m;j++) //从大小为1的背包处理到大小为m的背包
if(j>=w[i])//塞得进去这件物品的话
bag[i][j]=max(bag[i-1][j],bag[i-1][j-w[i]]+val[i]);//就判断是塞还是不塞比较好
else//塞不进去就是原来的值
bag[i][j]=bag[i-1][j];
printf("%d\n",bag[n][m]);//全部处理完后,输出最后的答案
return 0;
}
1.3 算法的空间优化
1.3.1 为什么要优化?
通过1.3的代码可以很容易的看出,当物品有1,000件,背包大小为1,000时,我们就需要开一个1,000,000大小的数组了,要是处理的数据再稍微大一点,就会导致数组开的过大,然后内存溢出。
所以为了解决更大的数据,空间的优化是必要的。
1.3.2 怎么优化?
我们可以发现,当我们处理第i件物品时,要用到的只是处理第i-1件物品时的数据,再之前的就用不到了。
所以,我们可以每次处理物品时,就把数据进行一次覆盖,这样二维数组就能降维到一维了。
但需要注意!降维到一维数组后,我们处理第i件物品时,就不能从大小为1的背包一直顺序处理到大小为m的背包了。为什么?因为这样的话,处理i-1件物品时的数据会在本次处理中被覆盖,而处理更大的背包时是会用到小背包的数据的。
这样就会出现——已经塞了第i件物品了,处理时就当做没塞,就会WA了。
所以我们这里进行倒序处理,因为处理小背包用不到大背包的数据,覆盖了也就覆盖了,无所谓。
1.3.3 优化代码
#include <stdio.h>
int max(int a,int b)
{
if(a>b) return a;
else return b;
}
int bag[1000];//降维到一维的数组
int w[1000],val[1000];//每件物品的大小与价值
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d%d",&w[i],&val[i]);//读入数据
for(int i=1;i<=n;i++) //从第一件物品一直处理到第i件物品
for(int j=m;j>0;j--) //倒序处理
if(j>=w[i])//塞得进去这件物品的话
bag[j]=max(bag[j],bag[j-w[i]]+val[i]);//就判断,处理后覆盖上一次处理的数据
else//塞不进去就是原来的值
bag[j]=bag[j];//这行代码完全没有意义,但是为了方便对比,留在这里
printf("%d\n",bag[m]);//全部处理完后,输出最后的答案
return 0;
}
优化完后,空间复杂度就从O(T*N)缩小到了O(N),大大节省了内存,也就可以处理更大的数据了。
1.4 0/1背包的常见例题及变种
1.4.1 采药问题 难度1 洛谷P1048
有M株药草,每株药草有其对应的价值及采摘该药草要花费的时间,在N时间内,求采摘的药草的最大价值。
这其实就是换了个题干的0/1背包模板题,上面的代码直接复制粘贴就可AC。
1.4.2 购物问题 难度1 洛谷P1060
你有N元钱,有M种物品,每种一件,每件物品都有对应的价格,并且你对每件物品都赋予了一个重要度(从1~5),求购买的物品的最高性价比和。
注:性价比=物品价格*重要度。
依然是换了个题干的0/1背包模板,只要把价值的计算变成物品价格*重要度即可(val[i] = w[i] * rank [i] )。
1.4.3 装箱问题 难度2 洛谷P1049
有一个容量为V的箱子,和n个物品,每个物品的体积为整数,装任意个物品,问剩余的最小体积是多少?
本题中,物品的价值与消费相同,也就是w[i]==val[i],代换进原来的代码即可。
注意最后输出的是剩余的最小体积,故应当输出V-bag[V]。
1.4.4 点菜问题 难度3 洛谷P1164
你有N元钱,有M种菜,每种只有一份,且有对应的价格。要求你把所有的钱都花光,问有多少种不同的点菜方案?
如果只有0元钱,那只有一种点菜方式——啥都不点。
接下来的递推公式为bag[money]+=bag[money - w[i] ]。
即花N元的点菜方式 = 花(N-某道菜价格)元钱的点菜方式的和。