转自:http://www.360doc.com/content/13/0705/12/13049620_297797824.shtml
//有N件物品和一个容量为V的背包。第i件物品的体积是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
//基本思路
//这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
//用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:
//f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
//这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为f[i-1][v];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f[i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。
//这时候我们可以用二维数组进行做了
/*任务:计算完全背包问题的最大价值
Sample Input
10 4
2 1
3 3
4 5
1 9
Sample Output
90
0 0 0 10
*/
#include<stdio.h>
#include
#include <iostream>
using namespace std;
#include
void print(int n, int m);
void printNumber(int n, int m);
int c[20][1000];//c[k][y]为只允许装前k种物品,背包总重量不超过y的最大价值
int inumber[21][1000];//inumber[k][u]为只允许装前K种物品, 背包总重量不超过u时得到最大价值时使用的背包的最大标号
int w[21],p[21];
int knapsack(int m,int n)
{
int i,j;
// for(i=1;i
// cin>>w[i]>>p[i];
memset(c,0,sizeof(c));
memset(inumber,0,sizeof(inumber));
for(j=1;j<m+1;j++)//设置边界值
{
c[1][j]=j/w[1]*p[1];
}
print( n, m);
printNumber(n,m);
for(i=1;i<n+1;i++)
{
for(j=1;j<m+1;j++)
{
if(j >= w[i])
{
//参考 金矿模型来说的 完全背包的判断如下:p[i]+c[i][j-w[i],解析为:对第i个矿进行开采,其价值为p[i],用去了w[i]个人,
//完全背包问题中每件物品有无限个,然后仍然对前i个矿进行开采,已知现在还可以用的人为 j-w[i],则c[i][j-w[i]]为其最大值
// c[i-1][j]:表示如果对第i个矿不开采,那么对前i-1个矿进行开采时,可以用的人数仍然是j个,则c[i-1][j]为其最大值
//,具体代码表示如下
// if(p[i]+c[i][j-w[i]]>=c[i-1][j])
{//和0-1背包相比只是将c[i-1][j-w[i]]写成了c[i][j-w[i]],因为完全背包问题中每件物品有无限个
// c[i][j]=p[i]+c[i][j-w[i]];
// inumber[i][j]=i;//记录对第i个金矿进行了开采,在使用inumber[i][j]的时候,只需统计减去w[i]*n(n∈1,2..n)
//后,在inumber中仍然是 i的个数,那个就是 第i个金矿被开发的次数
// }
//参考 金矿模型来说的
// 01背包的判断如下:p[i]+c[i-1][j-w[i],解析为:对第i个矿进行开采,其价值为p[i],用去了w[i]个人,
// ,然后对前i-1个矿进行开采,已知现在还可以用的人为 j-w[i],则c[i-1][j-w[i]]为其最大值
// c[i-1][j]:表示如果对第i个矿不开采,那么对前i-1个矿进行开采时,可以用的人数仍然是j个,则c[i-1][j]为其最大值
//以上是参考 金矿模型来说的,具体代码表示如下
if(p[i]+c[i-1][j-w[i]]>=c[i-1][j])
{
c[i][j]=p[i]+c[i-1][j-w[i]];
inumber[i][j]=i;
}
else
{
c[i][j]=c[i-1][j];
inumber[i][j]=inumber[i-1][j];//inumber[][]有两个作用,一是 做备忘,二是直接使用前面的备忘
//此处就是 二的作用,这个不同于金矿模型之处在于,
//金矿模型的 maxGold[max_n][max_people],将本例的 c[20][1000] 和inumber[21][1000]的功能和二为一
//max_people比真实的people多一个,这样造成maxGold多一列,就是灵活运用这多的一列来来完成inumber[][]功能
//这一列做备忘时,只是记录 可以开采出来的最大金子价值,二是要判断一下是否已经备忘过,如果有则使用,不是
//直接使用,是经过判断的; 还有就是 多的那一列,用于判断01背包的使用情况时,只需经过上下行的数值的不同
//就可以判断出使用情况,但不适用于完全背包,对于完全背包,最多只能求出金子的最大价值,而inumber[][]
//是可以求出使用情况的
}
}
else
{
c[i][j]=c[i-1][j];
inumber[i][j]=inumber[i-1][j];
}
}
print( n, m);
printNumber(n,m);
}
return(c[n][m]);
}
void print(int n, int m)
{
cout<<endl<< "void print() begin!!!!" <<endl;
cout << "备忘录的内容:" <<endl<<endl;
for(int i=1;i<n+1;i++)
{
for(int j=1;j<m+1;j++)
{
cout <<setw(4)<< c[i][j] <<"" ;
}
cout << endl;
}
cout<<endl<< "void print() end end end !!!!" <<endl<<endl;
}
void printNumber(int n, int m)
{
cout<<endl<< "void printNumber() begin!!!!" <<endl;
cout << "背包的标号" <<endl<<endl;
for(int i=1;i<n+1;i++)
{
for(int j=1;j<m+1;j++)
{
cout <<setw(4)<< inumber[i][j] <<"" ;
}
cout << endl;
}
cout<<endl<< "void printNumber() end end end !!!!" <<endl<<endl;
}
void trackSolution(int m, int n)
{
cout<<endl<< "void trackSolution(int m, int n) begin!!!!" <<endl;
int x[21];
int y = m;
int j = n;
memset(x, 0, sizeof(x));
/************************************************************************/
/* 难点:
第一轮 取 j=j1= inumber[][]最后一行最后一列的那个值,由于inumber[][]的值记录了对那个金矿进行了开采,所以此处的j,
就表示了对第j坐金矿进行了开采,原来有y=y_max=m个人,由于开本金矿,要用w[j]个人,还剩y = y_max - w[j]个人,
然后用这y = y_max - w[j]个人,仍然对这包括第j个金矿的前j个金矿进行开采,
如果inumber[j][y] ==inumber[j][y_max - w[j]]== j,表示第j座金矿又被开采此一次,
那么此时还剩下 y=y_max - w[j] - w[j],然后再次判断是否 inumber[j][y] ==inumber[j1][y_max - w[j]- w[j]]== j ,
如果仍然等于,则继续内层循环,序列数组x[j]++
否则 表示对第j个金矿的开采次数已经完全记录了下来,此时的j的值已经发生了变化,下面的记录已经与第j座金矿无关了,
如果此时的 inumber[j][y] 的值 ==0,表示 所有的金矿都已经统计了,表示inumber[j][y] ==0是数组边界,直接跳出外边循环,
否则 进行第二轮
第二轮 此处对inumber[][]是从最后一行进行倒序统计的,所以第二轮的j=j2肯定小于第一轮的j=j1,
j=j2=inumber[j2][y_max - w[j1]*x[j1]],所以此处的j, 就表示了对第j2坐金矿进行了开采,
原来有y=y2=y_max - w[j1]*x[j1] 个人,由于开本金矿,要用w[j2]个人,还剩y = y2 - w[j2]个人,
然后用这y = y2 - w[j2]个人,仍然对这包括第j2个金矿的前j2个金矿进行开采,后面的逻辑同于第一轮的逻辑,
总之,如果第二轮完不成,就进行三轮,依次类推。
*/
/************************************************************************/
while(true)
{
j = inumber[j][y];
x[j] = 1;
y = y - w[j];
while(inumber[j][y] == j)
{
y = y - w[j];
x[j]++;
}
printf("trackSolution()中的inumber[i][j] \n");
printNumber(n,m);
if(!inumber[j][y]) break;
}
printf("最大价值方案中各个物品的个数为(物品标号从1到n):");
for(j = 1; j <= n; j++)
{
printf("%d ", x[j]);
}
printf("\n");
cout<<endl<< "void trackSolution(int m, int n) end end end!!!!" <<endl;
}
int main()
{
freopen("beibao.in","r",stdin);
freopen("WQbeibao.out","w",stdout);
int m,n;
cin>>m>>n;
int i,j;
for(i=1;i<n+1;i++)
cin>>w[i]>>p[i];
printf("最大价值为%d\n",knapsack(m,n));
trackSolution(m, n);
return 0;
}
//beibao.in的内容如下:
//20 5
//17 92
//9 22
//4 80
//11 240
//19 90
//但是我们为了以后解决更加复杂的背包 必须学会用一维数组解决它
//
//先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1..N,每次算出来二维数组f[i][0..V]的所有值。那么,如果只用一个数组f[0..V],能不能保证第i次循环结束后f[v]中表示的就是我们定义的状态f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]两个子问题递推而来,能否保证在推f[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]和f[i-1][v-c[i]]的值呢?事实上,这要求在每次主循环中我们以v=V..0的顺序推f[v],这样才能保证推f[v]时f[v-c[i]]保存的是状态f[i-1][v-c[i]]的值
//
//首先想想为什么01背包中要按照v=V..0的逆序来循环。这是因为要保证第i次循环中的状态f[v]是由状态f[v-c]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个没有已经选入第i件物品的子结果f[v-c[i]]。
//
//而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[v-c],所以就可以并且必须采用v=0..V的顺序循环。这就是这个简单的程序为何成立的道理。
//
//)。
//
//如果将v的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v]由f[i][v-c[i]]推知
//
//
//
//(因为假设逆序,对于01背包来说,n=5,当 i=2,v=V..0=10….0,
//此时f[10]已经有值,其值是i=1时求出的,
//f[10-c[2]]也有值,其值也是i=1时求出的;
//
//但是如果是顺序的话,
//那么当 i=2,v=0….V=0….10,此时f[10-c[2]]一定在f[10]前求出值,
//也就是说,在求f[10]时,
//f[10]=max(f[10-c[2]]的i=2时的求出值,f[10] 有值,其值是i=1时求出的),
//
//这样的结果不是想要的结果,
//那么有人问为什么用二维数组时,可以顺序求解呢,那是因为用二维数组时,,f[i][v-c[i]],f[i-1][v-c[i]],
//f[i][v],f[i-1][v]都分别存储在不同的地方,只需通过i和i-1来控制就可以取到需要的值,
//而在一维数组中f[i][v-c[i]],f[i-1][v-c[i]],他们最终都是在f [v-c[i]]这一个位置上,是会覆盖的,f[i][v],f[i-1][v]同此理。)
//与本题意不符,但它却是另一个重要的背包问题完全背包最简捷的解决方案,故学习只用一维数组解01背包问题是十分必要的。
//伪代码如下:
//for i=1..N
// for v=V..0
// f[v]=max{f[v],f[v-c[i]]+w[i]};
//画个图给大家演示下
//也就是说此时的f[v],f[v-c[i]] 是前面的
//假设体积是10
//
//
// 背包体积----->>>>>
//价值 大小 1 2 3 4 5 6 7 8 9 10
// 2 1 2 2 2 2 2 2 2 2 2 2 放 第 一 个 物 品 (第一决策阶段)
// 1 2 2 2 3 3 3 3 3 3 3 3 放 第 二 个 (第二决策阶段)
// 5 3 2 2 5 7 7 8 8 8 8 8
// 4 4 2 2 5 7 7 8 8 8 8 12
// 3 5
// 上表的理解很重要! 每一行代表每个决策阶段!首先,看“背包体积”,当体积为1时,看物品的”大小“,第一决策阶段对应的物品能装下,体积是1,
//价值是2,在表中”背包体积“和”大小“交集的格子填2;这样,在下面更加大的”背包体积“也能装的下"大小"为1的物品,那在第一行(第一决策阶段)
//的剩下的格子也填2;那么到了第二个决策阶段,这里有个重点,就是程序在执行的时候,会先初始化整个一行,是这样初始化的:就是要填的那个
//格子的相邻左和上比较,较大者初始化将要填的格子;然后到了真正要填的时候,看对应决策阶段的物品”大小“和对应的”背包体积“,如果”背包体积“
//小于物品”大小“,那么,格子的最终值是前面默认的,不要改;如果”背包体积“大于物品”大小“,那么将”背包体积“减去物品”大小“所得的值(视作”背
//包体积“)对应的上一阶段的最优解(已填的格子)(在之前的计算已经得出)加上现在的物品”大小“所对应的”价值“,这个值与之前默认的值比较,
//谁较大,谁就是最终的格子的值。 比如:第三决策阶段,第6列的格子的最终值8是怎么来的呢?是这样的,首先初始化默认的值,将相邻的左边7
//和上边3比较,7较大,那么,这个格子的默认初始值就是7。然后到了填这个格子,对应的”背包体积“是6,物品”大小“的3。6>3,那么,6-3=3,这
//个3”背包体积“所对应的上一阶段的最优解为3,然后5加上物品”大小“3本身对应的价值5,即5+3=8,而8>7(默认),所以,最终的最优解为8,所以这
//个格子填8。
//可以看出 f[v]=max{f[v],f[v-c[i]]+w[i]};
//V是从大到小的
//其中的f[v],f[v-c[i]]是前面的值 比较的结果赋值给f[v]
//比如说当i=5的时候 f[v]>f[v-5]注意两者都是i=4时候的值得出的结果12 赋值到新的f[v] 但是此时的f[v]是i=5的
//当把最后一行改写成55 6的时候 我们可以看出 f[v-6]=f[4]+55 大于f[v] 所以我们可以得出新的f[v]=f[4]+55这样结果就是取的体积为1 2 6
//代码如下//一维数组实现背包问题
#include <stdio.h>
#include <string.h>
#define N 3500
#define M 13000
int d[N],w[N],val[M],n,W;
void knapsack()
{
int i,j;
memset(val,0,sizeof(val));
for(i=1;i<=n;i++)
for(j=W;j>=1;j--)
{//当前背包容量比第i件物品体积大,且当前价值比更新后价值小,则更新
if(j>=w[i]&&val[j-w[i]]+d[i]>val[j])
val[j]=val[j-w[i]]+d[i];
}
return ;
}
int main()
{
freopen("01beibao_一维.in","r",stdin);
int i;
while(scanf("%d%d",&n,&W)!=EOF)
{
for(i=1;i<=n;i++)
scanf("%d%d",&w[i],&d[i]);
knapsack();
printf("%d\n",val[W]);
}
return 0;
}
//01beibao_一维.in内容如下:
//5 20
//17 92
//9 22
//4 80
//11 240
//19 90
//初始化的细节问题
//我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
//如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
//如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。
//为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。
//这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。
//一个常数优化
//前面的伪代码中有 for v=V..1,可以将这个循环的下限进行改进。
//由于只需要最后f[v]的值,倒推前一个物品,其实只要知道f[v-w[n]]即可。以此类推,对以第j个背包,其实只需要知道到f[v-//sum{w[j..n]}]即
//可,即代码中的
//for i=1..N
// for v=V..0
//可以改成
//for i=1..n
//bound=max{V-sum{w[i..n]},c[i]}
//for v=V..bound
//这对于V比较大时是有用的。
//小结
//01背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,另外,别的类型的背包问题往往也可以转
//成01背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及最后怎样优化的空间复杂度。
//
//
//--如果对 01背包的二维数组实现方式不清晰,可以参考 金矿模型