0-1揹包问题

 

题目
有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]。
  阶段 I (物品的数量)
  每个阶段的状态: 前i个物品 总容量不超过 v的 最大价值  f[i][v]
  状态转移方程   
当v大于c[i]  f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
当v小于c[i]  f[i][v] = f[i-1][v]
 边界控制   I =0  或 v=0 时 f[i][v] =0
for i:=1 to v do f[0,i]:=0;
for i:=1 to n do f[i,0]:=0;
for i:=1 to n do
for j:=1 to v do
begin
if j>=c[i] then f[i,j]:=max(f[i-1,j-c[i]]+w[i],f[i-1,j])
else f[i,j]:=f[i-1,j];
end;

   
优化空间复杂度
以上方法的时间和空间复杂度均为O(VN),其中时间复杂度应该已经不能再优化了,但空间复杂度却可以优化到O。
先考虑上面讲的基本思路如何实现,肯定是有一个主循环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]]的值。伪代码如下:
for i=1..N
    for v=V..0
        f[v]=max{f[v],f[v-c[i]]+w[i]};

其中的f[v]=max{f[v],f[v-c[i]]}一句恰就相当于我们的转移方程f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]},因为现在的f[v-c[i]]就相当于原来的f[i-1][v-c[i]]。如果将v的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v]由f[i][v-c[i]]推知,与本题意不符,但它却是另一个重要的揹包问题完全揹包最简捷的解决方案,故学习只用一维数组解01揹包问题是十分必要的。
事实上,使用一维数组解01揹包的程序在后面会被多次用到,所以这里抽象出一个处理一件01揹包中的物品过程,以后的代码中直接调用不加说明。
过程ZeroOnePack,表示处理一件01揹包中的物品,两个参数cost、weight分别表明这件物品的费用和价值。
procedure ZeroOnePack(cost,weight)
    for v=V..cost
        f[v]=max{f[v],f[v-cost]+weight}
注意这个过程里的处理与前面给出的伪代码有所不同。前面的示例程序写成v=V..0是为了在程序中体现每个状态都按照方程求解了,避免不必要的思维复杂度。而这里既然已经抽象成看作黑箱的过程了,就可以加入优化。费用为cost的物品不会影响状态f[0..cost-1],这是显然的。
有了这个过程以后,01揹包问题的伪代码就可以这样写:
for i=1..N
    ZeroOnePack(c[i],w[i]);
初始化的细节问题
我们看到的求最优解的揹包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满揹包”时的最优解,有的题目则并没有要求必须把揹包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满揹包,那么在初始化时除了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揹包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及最后怎样优化的空间复杂度。


举例:

/* 一个旅行者有一个最多能用M公斤的揹包,现在有N件物品,
它们的重量分别是W1,W2,…,Wn,
它们的价值分别为P1,P2,…,Pn.
若每种物品只有一件求旅行者能获得最大总价值。
输入格式:
M,N
W1,P1
W2,P2
……
输出格式: 

*/

因为揹包最大容量M未知。所以,我们的程序要从1到M一个一个的试。比如,开始任选N件物品的一个。看对应M的揹包,能不能放进去,如果能放进去,并且还有多的空间,则,多出来的空间里能放N-1物品中的最大价值。怎么能保证总选择是最大价值呢?看下表。
测试数据:
10,3
3,4
4,5
5,6

《0-1揹包问题》

c[i][j]数组保存了1,2,3号物品依次选择后的最大价值.

这个最大价值是怎么得来的呢?从揹包容量为0开始,1号物品先试,0,1,2,的容量都不能放.所以置0,揹包容量为3则里面放4.这样,这一排揹包容量为4,5,6,….10的时候,最佳方案都是放4.假如1号物品放入揹包.则再看2号物品.当揹包容量为3的时候,最佳方案还是上一排的最价方案c为4.而揹包容量为5的时候,则最佳方案为自己的重量5.揹包容量为7的时候,很显然是5加上一个值了。加谁??很显然是7-4=3的时候.上一排 c3的最佳方案是4.所以。总的最佳方案是5+4为9.这样.一排一排推下去。最右下放的数据就是最大的价值了。(注意第3排的揹包容量为7的时候,最佳方案不是本身的6.而是上一排的9.说明这时候3号物品没有被选.选的是1,2号物品.所以得9.)

从以上最大价值的构造过程中可以看出。

f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}这就是书本上写的动态规划方程.这回清楚了吗?

#include<stdio.h> int c[10][100];/*对应每种情况的最大价值*/ int knapsack(int m,int n) { int i,j,w[10],p[10]; for(i=1;i<n+1;i++) scanf(“/n%d,%d”,&w[i],&p[i]); for(i=0;i<10;i++) for(j=0;j<100;j++) c[i][j]=0;/*初始化数组*/ for(i=1;i<n+1;i++) for(j=1;j<m+1;j++) { if(w[i]<=j) /*如果当前物品的容量小于揹包容量*/ { if(p[i]+c[i-1][j-w[i]]>c[i-1][j]) /*如果本物品的价值加上揹包剩下的空间能放的物品的价值*/ /*大于上一次选择的最佳方案则更新c[i][j]*/ c[i][j]=p[i]+c[i-1][j-w[i]]; else c[i][j]=c[i-1][j]; } else c[i][j]=c[i-1][j]; } return(c[n][m]); } int main() { int m,n;int i,j; scanf(“%d,%d”,&m,&n); printf(“Input each one:/n”); printf(“%d”,knapsack(m,n)); printf(“/n”);/*下面是测试这个数组,可删除*/ for(i=0;i<10;i++) for(j=0;j<15;j++) { printf(“%d “,c[i][j]); if(j==14)printf(“/n”); } system(“pause”); }  举例:

HDU 2599 Robberies 

Problem Description

The aspiring Roy the Robber has seen a lot of American movies, and knows that the bad guys usually gets caught in the end, often because they become too greedy. He has decided to work in the lucrative business of bank robbery only for a short while, before retiring to a comfortable job at a university.


《0-1揹包问题》

For a few months now, Roy has been assessing the security of various banks and the amount of cash they hold. He wants to make a calculated risk, and grab as much money as possible.

His mother, Ola, has decided upon a tolerable probability of getting caught. She feels that he is safe enough if the banks he robs together give a probability less than this.

 

Input


The first line of input gives T, the number of cases. For each scenario, the first line of input gives a floating point number P, the probability Roy needs to be below, and an integer N, the number of banks he has plans for. Then follow N lines, where line j gives an integer Mj and a floating point number Pj . 
Bank j contains Mj millions, and the probability of getting caught from robbing it is Pj .

 

Output


For each test case, output a line with the maximum number of millions he can expect to get while the probability of getting caught is less than the limit set.

Notes and Constraints
0 < T <= 100
0.0 <= P <= 1.0
0 < N <= 100
0 < Mj <= 100
0.0 <= Pj <= 1.0
A bank goes bankrupt if it is robbed, and you may assume that all probabilities are independent as the police have very low funds.

 

Sample Input

3 0.04 3 1 0.02 2 0.03 3 0.05 0.06 3 2 0.03 2 0.03 3 0.05 0.10 3 1 0.03 2 0.02 3 0.05

 

Sample Output

2 4 6


 

解题思路是:把银行中的总钱数当做揹包的容量,得到得价值就是没被抓的情况下所偷到的

钱数,相应花费的代价就是被抓的概率,这样问题就抽象成了在最大花费的情况下,所能获得的

最大价值!

在这样的分析以后,不难写出状态转移方程:


opt[j]=max(opt[j],opt[j-b[i].money]*(1-b[i].p))


其中,opt[j]是当偷到j钱时,不被抓的最大的概率

算法实现:

#include<iostream> using namespace std; const int INF=10010; struct bank { int money; double p; //p表示被抓到的概率 }b[101]; int main() { double opt[INF],p; int t,n,total,i,j,flag; scanf(“%d”,&t); while(t–) { scanf(“%lf%d”,&p,&n); total=0; for(i=0;i<n;i++) { scanf(“%d%lf”,&b[i].money,&b[i].p); total+=b[i].money; //total为揹包的容量 } opt[0]=1; //这个初始化是很重要的! for(i=1;i<=total;i++) opt[i]=0; for(i=0;i<n;i++) for(j=total;j>=b[i].money;j–) opt[j]=max(opt[j],opt[j-b[i].money]*(1-b[i].p)); flag=1; for(i=total;i>=0&&flag;i–) //求最优解 if(opt[i]>=1-p) flag=0,printf(“%d/n”,i); } return 0; }


解法2:


因为要算小于给出的 概率P 的情况下能抢到多少钱, 所以在对每家银行实行抢劫的时候, 有其中一家银行失败就不行了, 所以只需要要算全部都成功的情况就行了 . 状态转移方程 : dp[i] = max { dp[i], dp[i-w[j]] * v[j] } , dp[i] 表示成功偷到 i 快 钱的概率, w v 分别代表 银行的存款 和 成功偷取的概率. 其实就是 01 揹包

 

实现:

#include <iostream> #include <string> using namespace std; int T, N, M, sum, w[110]; double dp[10010] = { 1 }, v[110], res; int main () { scanf ( “%d”, &T ); while ( T — ) { scanf ( “%lf”,&res ); scanf ( “%d”, &N ); sum = 0, res = 1 – res; for ( int i = 1; i <= N; ++ i ) { scanf ( “%d”,&w[i] ); scanf ( “%lf”, &v[i] ); sum += w[i]; } memset ( dp, 0, sizeof ( dp ) ); dp[0] = 1; for ( int i = 1; i <= N; ++ i ) { for ( int j = sum; j >= w[i]; — j ) { dp[j] = max ( dp[j], dp[j-w[i]] * ( 1 – v[i] ) ); } } for ( int i = sum; i >= 0; — i ) { if ( dp[i] >= res ) { printf ( “%d/n”, i ); break; } } } return 0; }


参考:

http://www.baiyun.me/2011/02/10/hdu-2955-robberies-%E6%A6%82%E7%8E%87dp.htm

http://blog.csdn.net/sunrisemaple/archive/2009/06/12/4264278.aspx

http://blog.csdn.net/adcxf/archive/2008/08/07/2784156.aspx

点赞