一:01背包问题
题目:有N件物品和一个容量为V的背包。第i件物品的费用是c,价值是w。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本思路:这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即f[v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:f[v]=max{f[v],f[v-c]+w}。这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。 如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”;如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为 v-c的背包中”,此时能获得的最大价值就是f [v-c]再加上通过放入第i件物品获得的价值w。注意f[v]有意义当且仅当存在一个前i件物品的子集,其费用总和为v。所以按照这个方程递推完毕后,最终的答案并不一定是f[N] [V],而是f[N][0..V]的最大值。如果将状态的定义中的“恰”字去掉,在转移方程中就要再加入一项f[v-1],这样就可以保证f[N] [V]就是最后的答案。以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V)。先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1..N,每次算出来二维数组 f[0..V]的所有值。那么,如果只用一个数组f [0..V],能不能保证第i次循环结束后f[v]中表示的就是我们定义的状态f[v]呢?f[v]是由f[v]和f [v-c]两个子问题递推而来,能否保证在推f[v]时(也即在第i次主循环中推f[v]时)能够得到f[v]和f[v -c]的值呢?事实上,这要求在每次主循环中我们以v=V..0的顺序推f[v],这样才能保证推f[v]时f[v-c]保存的是状态f[v-c]的值。 其中的f[v]=max{f[v],f[v-c]}一句恰就相当于我们的转移方程 f[v]=max{f[v],f[v-c]},因为现在的f[v-c]就相当于原来的f[v-c]。
总结:01背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,另外,别的类型的背包问题往往也可以转换成01背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及最后怎样优化的空间复杂度。注意,第二重for循环是由大到小,一些细节处理,注重前缀和和区间和等思想!
例题:
poj3624 Charm Bracelet
有N件物品和一个容量为V的背包。第i件物品的费用是c,价值是w。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
空间优化:
for i=1..N
for v=V..0
f[v]=max{f[v],f[v-c[i]]+w[i]};
初始化细节:
若要求恰好装满背包,初始化时除了f[0]为0其它f[1..V]均设为-∞
若没有要求必须把背包装满,初始化时将f[0..V]全部设为0
空间优化:
for i=1..N
for v=V..0
f[v]=max{f[v],f[v-c[i]]+w[i]};
初始化细节:
若要求恰好装满背包,初始化时除了f[0]为0其它f[1..V]均设为-∞
若没有要求必须把背包装满,初始化时将f[0..V]全部设为0
#include<iostream>
using namespace std;
#define MAX_N 3500
#define MAX_V 20000
struct good
{
int c;
int w;
}goods[MAX_N];
int main()
{
int i,j;
int n,v;
int f[MAX_V];
while(cin>>n>>v)
{
for(i=0;i<=v;i++)f[i]=0;
for(i=0;i<n;i++)cin>>goods[i].c>>goods[i].w;
for(i=0;i<n;i++)
{
for(j=v;j>=goods[i].c;j--)
{
if(f[j]<f[j-goods[i].c]+goods[i].w)
f[j]=f[j-goods[i].c]+goods[i].w;
}
}
cout<<f[v]<<endl;
}
return 0;
}
二:
完全背包
问题
题目:有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c,价值是w。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本思路:这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑, 与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[v]表示前i种物品恰放入一个容 量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:f[v]=max{f[v-k*c]+k*w|0<=k*c& lt;= v}。若两件物品i、j满足c<=c[j]且 w>=w[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。 对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。另考虑到第i种物品最多选V/c 件,于是可以把第i种物品转化为V/c件费用及价值均不变的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。
最高效:把第i种物品拆成费用为c*2^k、价值为w*2^k的若干件物品,其中 k满足c*2^k<V。这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。
总结:全背包问题也是一个相当基础的背包问题。事实上,对每一道动态规划题目都思考其方程的意义以及如何得来,是加深对动态规划的理解、提高动态规划功力的好方法。注意,第二次循环是从小到大(固定模式)
三.多重背包
题目:有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大 。
题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i 种物品有n+1种策略:取0件,取1件……取 n件。令f[v]表示前i种物品恰放入一个容量为v的背包的最大权值,则:f[v]=max{f[v-k*c]+ k*w|0<=k<=n}
方法:
将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来 的费用和价值乘以这个系数。使这些系数分别为 1,2,4,...,2^(k-1),n-2^k+1,且k是满足n-2^k+1>0的最大整数。例如,如果n为13,就将这种物品分成系数分别为 1,2,4,6的四件物品。
总结:注意“拆分物品”的思想和方法!
POJ1276-Cash Machine
题意大致是给出一个数CASH,再给出N种钱,dk价值的钱nk个。要你求小于CASH的情况下最多可以支付多少钱。
因为每种货币的面值及数量已知,可以将其转化为多重背包,背包的容量即为cashdp数组记录i价值的钱数是否可以拼凑成功。dp[ i ] =dp[ i ] && dp[ i-dk*x ];而x的大小有限,需要用use数组记录dk在凑成 i 价值时使用次数。
最后只需在dp[] 里面找出最大的i是dp[i]=1就行。
int main(){
// freopen ("g.txt","r",stdin);
int cash;
int N;
int nk[1010],dk[1010];
int dp[100010],use[100010];
while ( ~ scanf ("%d%d",&cash,&N)){
memset(dp,0,sizeof(dp));
for (int i=0;i<N;i++){
cin>>dk[i]>>nk[i];
}
dp[0]=1;
for (int i=0;i<N;i++){
memset(use,0,sizeof(use));
for (int j=dk[i];j<=cash;j++){
if (!dp[j]&&dp[j-dk[i]]&&use[j-dk[i]]<nk[i]){
dp[j]=1;
use[j]=use[j-dk[i]]+1;
}
}
}
for (int i=cash;i>=0;i--){
if (dp[i]){
cout<<i<<endl;
break;
}}}
return 0;
}
优化:
用二进制思想。注意这儿要2^k中k的取值。大致取值意思为
k=0;sum=0;cnt=0;
while(1)
if(sum+2^k>n[i]) break; //
zopack[…][cnt++]=2^k;
sum+=2^k;
zopack[…][cnt++]=n[i]-sum; //
这里对于每一个n[i]都有一个zopack[];然后将每一zopack[][]看作01背包的每一个物品就可以按照01背包的思想做了。
四.分组的背包问题
题目:有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
算法:这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值 状态方程:f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于组k}
分组的背包问题将彼此互斥的若干物品称为一个组,问题每一组到底选哪一个物品!
伪码:
for 所有的组k
for v=V..0
for 所有的i属于组k
f[v]=max{f[v],f[v-c[i]]+w[i]}仍然可以对每组中做像完全背包那样的优化
注意:“for v=V..0”这一层循环必须在“for 所有的i属于组k”之外。这样才能保证每一组内的物品最多只有一个会被添加到背包中。
HDU 1712 ACboy needs your help
题目大意:给定n门课,再用m天复习这n门课,然后给定一个n*m的矩阵A,A[i][j]表示用j天复习第i门课获得得收益。问用m天复习不同的课获得的最大收益。
将天数m作为背包的容量,科目数目作为背包的种类数目,天数j作为背包的重量,因为一个科目只能选一次(复习天数),对应于每组中的物品只能选一件,正好是分组背包问题。转移方程:dp[j]=max(dp[j-k]+a[i][k],dp[j])
代码:
#include<stdio.h>
#include<string.h>
int dp[105],a[105][105];
int max(int a,int b)
{
if(a<b)
return b;
return a;
}
int main()
{
int i,j,k,n,m;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==0&&m==0)
break;
memset(dp,0,sizeof(dp));
for(i=0;i<n;i++)
for(j=1;j<=m;j++)
scanf("%d",&a[i][j]);
for(i=0;i<n;i++)
for(j=m;j>=0;j--)
for(k=1;k<=j;k++)
dp[j]=max(dp[j],dp[j-k]+a[i][k]);
printf("%d\n",dp[m]);
}
return 0;
}
注意细节处理,前缀合区间和的运用,以及二进制思想,大大优化程序!