0-1背包问题动态规划详解
最近开始学习背包问题,在此做些笔记,先学习最简单的0-1背包问题。
0-1背包问题介绍:
一,背包问题基本解决
有一个背包可以存放M斤物品,有N件物品(每件物品只有1件),他们重量分别是w1,w2,w3..........,他们价值分别是p1,p2,p3...............。问怎么装载物品,使背包装的载物品价值最大?
举例说明:
背包装10斤物品,有3件物品,重量分别是3斤,4斤,5斤,价值分别是4,5,6;
可以画一个矩阵,行号0,1,2,3,行号0表示0件物品可放入背包情况下,背包装载最大价值,行号1表示只有第一件物品可以放入背包情况下,背包装载最大价值,行号2表示只有第1件和第2件可以放入背包情况下,背包装载最大价值,......
列号0,1,2,3.............10表示背包剩余可用容量,列号0表示背包已满时候,剩余容量为0时候,还可以装载的最大价值;列号1表示背包剩余容量为1时候,还可以装载的最大价值,......
当行号是3,列号是10时候,价值最大,即c[3][10]值最大.
行号是0,表示有0件物品可放入背包,此时背包装载0件物品,因此价值都为0,即c[0][0]=c[0][1]=.....c[0][10]=0;
行号是1,表示第一件物品可以放入背包,第一件物品重量是3,当列号为0,1,2时候,一件物品也放不下,背包装载价值是0,当列号3,4,5...........10可以放下,但是只有一件物品,因此最多获取价值是4,即c[1][3]=c[1][4]..........c[1][10] = 4;
行号是2,表示第一件和第二件物品可以放入背包,第一件物品重3斤,第二件重4斤,当列号0,1,2时候,一件物品也放不下,背包装载价值是0,当列号为3时候,可以放下第一件,但是放不下第二件,价值是4,列号为4时候,可以放下第一件也可以放下第二件,此时分为两种情况,放第二件或不放第二件,取两者最大值即可。放第二件,剩余容量是0,此时价值是p[2]+容量0,行号是1时最大值,即p[2]+c[1][0]=5,不放第二件,则是剩余容量是4,行号是1时,最大值,即c[1][4] = 4,则5>4,我们存放第二件,此时背包装载最大价值是5,当列号为5,6时候,价值5,当列号为7时候,max{不放第二件,放第二件},不放第二件,即列号为7,行号为1最大值,c[1][7]=4;放第二件,为:p[2]+列号为7-w[2]=7-4=3,行号为1时最大值,为:p[2]+c[1][3]=5+4=9,9>4,我们选择存放第二件,此时装载了两件物品,列号8,9,10也是9.
当行号是2时,我们可以得出规律:最大值=max{存放第二件,不存放第二件}=max{p[2]+c[1][列号-w[2]],c[1][列号]},而列号就是从1到背包容量V之间某个值。
当行号是1时,我们可以得出规律:最大值=max{存放第一件,不存放第一件}=max{p[1]+c[0][列号-w[1]],c[0][列号]}=max{p[1],0},当列号>=w[1],最大值p[1],否则0。
我们由此得出一个疑问:为什么列号是0,1,2....10,可以这样(列号=10不变)吗?因为我们只求背包剩余容量为10时,装载价值最大值。
答案是不可以的,比如求当行号是3,列号是10最大值,即最大值=c[3][10] = max{p[3]+c[2][5],c[2][10]},欲求c[3][10]必须知道c[2][5]和c[2][10],而c[2][5] = max{p[2] + c[1][1],c[1][5]},欲求c[2][5]必须求c[1][1]和c[1][5],我们求c[i][10]时候会用到c[i][j],此时0<=j<=10,因此我们必须从行号是0开始,把列号0......10之间,所有c[i][j]求出来,因为后面要用到,这是一个不断递推过程。
因此,得出结论:
将前i件物品放入背包中,求背包装载最大价值问题。只考虑第i件物品策略(第i件放还是不放),如果不放第i件物品,求前 i-1 件物品放入容量背包,背包装载最大价值问题,最大价值为c[i-1][v];如果放第i件物品,求 i-1 件物品放入容量 v-w[i] 容量背包,背包装载最大价值问题,最大价值为p[i] +c[i-1][v-w[i-1]];
因此c[i][j] = max{c[i-1][v],p[i]+c[i-1][v-w[i-1]]};
因此我们得出核心代码:
for (int i = 1; i <= N; i++){ for(int j = 1; j <= M; j++){ if (c[i][j] >= w[i]){ if (c[i-1][j] > p[i]+c[i-1][j-w[i]]){ c[i][j] = c[i-1][j]; }else{ c[i][j] = p[i] + c[i-1][j-w[i]]; } }else{ c[i][j] = c[i-1][j]; } } }
下面是用C++简单实现程序(VS2008通过):
1 #include <cstdio>
2 const int K = 100;
3
4
5 /*
6 输入格式:
7 10 3 //M=10,N=3
8 3 4 //w[1]=3,p[1]=4
9 4 5 //w[2]=4,p[2]=5
10 5 6 //w[3]=5,p[3]=6
11 */
12
13 int max(int a,int b){
14 return a>b?a:b;
15 }
16 int main(){
17 int M,N,w[K],p[K],i,j,c[K][K];
18 scanf("%d%d",&M,&N);
19 for (i = 1;i <= N;i++)
20 {
21 scanf("%d%d",&w[i],&p[i]);
22 }
23 for (i = 0;i <= N;i++)
24 {
25 for (j = 0;j <= M;j++)
26 {
27 c[i][j] = 0;
28 }
29 }
30
31 for (i = 1;i <= N;i++)
32 {
33 for (j = 1;j <= M;j++)
34 {
35 if (w[i] <= j)
36 {
37 c[i][j] = max(c[i-1][j],c[i-1][j-w[i]]+p[i]); //方式1
38 /* if (c[i-1][j] > c[i-1][j-w[i]]+p[i]) //方式2
39 {
40 c[i][j] = c[i-1][j];
41 }else{
42 c[i][j] = c[i-1][j-w[i]]+p[i];
43 }
44 */
45 }else{
46 c[i][j] = c[i-1][j];
47 }
48 }
49 }
50
51 printf("%d\n",c[N][M]);
52 return 0;
53 }
运行结果:
二,背包问题优化解决
在以上解决方案中,所用时间复杂度是O(M*N),空间复杂度是O(M*N),用到了一个M*N的数组。如果仔细观察之后,发现可以用一维数组代替二维数组。
当行号为0时,数组c[0][0]=c[0][1]=...................=c[0][10] = 0;
当行号为1时,根据c[i][j] = max{c[i-1][j],p[i]+c[i-1][j-w[i]]},得c[1][j]=max{c[0][j],p[1]+c[0][j- w[1]]}。即c[1][0,1,2.......,9,10]由c[0][0,1,2.......,9,10]得到。
因此我们可以利用a[j]表示c[0][j],用b[j]表示c[1][j],首先b[0]=a[0] = 0,b[1]=a[1]=0,.........b[10]=a[10]=0。将数组b和数组a值设为一样。然后如果w[i]< j,再根据公式b[j]=max{a[j],p[1]+a[j-w[1]]},重新确定b[j]的值,如果w[i] >= j,则不变。
但是我们仔细观察一下次公式b[j]=max{a[j],p[1]+a[j-w[1]]},发现b[j]得值由a[k],p[1]和w[1]所得,而k的 取值范围是0<=k<=j,即b[j]由数组a中下标小于j的元素得到。因此我们可以将j从大到小使用来得到b[j],当i = 0时候,a[0] = a[1] = a[2]=a[3]=..............=a[10]=0,当i=1时,先设定 b[0] = a[0],b[1]=a[1],b[2]=a[2],b[3]=a[3],..............=b[10]=a[10],我们先计算 b[10],b[10]=max{a[10],p[1]+a[10-w[1]]}=max{a[10],4+a[7]},此处 p[1]=4,w[1]=3;因为a[10]=b[10],a[7]=b[7],因此b[10]=max{b[10],4+b[7]}=4,至此数组b和 数组a只有第10个元素值不想等,其他元素都相等;b[9]=max{a[9],p[1]+a[9- w[1]]}=max{a[9],4+a[6]},a[9]=b[9],a[6]=b[6],因此b[9]=max{b[9],b[6]+4}=4,至此 数组b和数组a只有第9,10个元素值不想等,其他元素都相等;因此求的 b[8]=max{b[8],4+b[5]},b[7]=max{b[7],4+b[4]}.............b[3]=max{b[3],4+b[0]}。 在求解过程中,我们发现只要按数组b元素下标从大到小求解,就可以用自己求的自己的值。
至此我们可以利用一个一维数组代替二维数组。
核心代码:
for (int i = 1; i <= N; i++){ for(int j = M; j > 0; j--){ if (c[i] <= j){ if (c[j] > p[i]+c[j-w[i]]){ c[j] = c[j]; }else{ c[j] = p[i] + c[j-w[i]]; } } } }
下面是用C++简单实现程序(VS2008通过):
1 #include <cstdio>
2 const int K = 100;
3
4
5 /*
6 输入格式:
7 10 3 //M=10,N=3
8 3 4 //w[1]=3,p[1]=4
9 4 5 //w[2]=4,p[2]=5
10 5 6 //w[3]=5,p[3]=6
11 */
12
13 int max(int a,int b){
14 return a>b?a:b;
15 }
16
17 int main(){
18 int M,N,w[K],p[K],i,j,f[K] = {0};
19 scanf("%d%d",&M,&N);
20 for (i = 1;i <= N;i++)
21 {
22 scanf("%d%d",&w[i],&p[i]);
23 }
24
25 for (i = 1;i <= N;i++)
26 {
27 for (j = M;j > 0;j--)
28 {
29 if (w[i] <= j)
30 {
31 f[j] = max(f[j],p[i]+f[j-w[i]]);
32 }
33 }
34 }
35
36 printf("%d\n",f[M]);
37 return 0;
38 }
运行结果: