经典DP 背包问题
三种基本背包问题的概念
01背包(Zero one pack):
有N件物品和一个容量为V的背包, 每种物品均只有一件。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
完全背包(Complete Pack):
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,
且价值总和最大。
多重背包(Multiple Pack):
有N种物品和一个容量为V的背包,第i种物品最多有n[i]件可用。每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且
价值总和最大。
01背包
1背包(ZeroOnePack): 有N件物品和一个容量为V的背包,每种物品均只有一件。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。
其状态转移方程是:
把这个过程理解下:
在前i件物品放进容量v的背包时,它有两种情况
PS:第二种是如果第i件放进去,那么在容量v-c[i]里就要放进前i-1件物品
最后比较第一种与第二种所得价值的大小,哪种相对大,f[i][v]的值就是哪种。
这里是用二维数组存储的,可以把空间优化,用一维数组存储。
代码如下:(外循环正序 内循环逆序 使f[v]和f[v-c[i]]+w[i]表示前一状态的价值)
1 2 3
| for i=1..N for v=V..0 f[v]=max{f[v],f[v-c[i]]+w[i]}
|
完全背包
完全背包(CompletePack): 有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费
用总和不超过背包容量,且价值总和最大。
完全背包按其思路仍然可以用一个二维数组来写出:(两种状态方程)
同样的它也可以用一维数组来表示
代码如下:(外循环和内循环均正序 保证使f[v]和f[v-c[i]]+w[i]表示当前状态的价值)
1 2 3
| for i=1..N for v=0..V f[v]=max{f[v],f[v-c[i]]+w[i]}
|
多重背包
多重背包(MultiplePack): 有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和
不超过背包容量,且价值总和最大。
其状态转移方程是:
多重背包问题可以转化为01背包问题:(提供如下模版)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void ZeroOnePack(int value,int weight) { for(int v=m;v>=value;v--) dp[v]=max(dp[v-value]+weight,dp[v]); } void CompletePack(int value,int weight) { for(int v=value;v<=m;v++) dp[v]=max(dp[v],dp[v-value]+weight); } void MultiPack(int value,int weight,int amount) { if(value*amount>=m) { CompletePack(value,weight); return ; } for(int k=1;k<amount;k*=2) { ZeroOnePack(k*value,k*weight); amount-=k; } ZeroOnePack(amount*value,amount*weight); } value表示每件的花费 weight表示每件的价值 amount表示每件可取的数量 m表示最大背包容量 具体在1059题中使用
|
也可变形为以下模版,性质不变:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void ZeroOnePack(int value,int cost) { for(int v=m;v>=cost;v--) dp[v]=max(dp[v-value]+cost,dp[v]); } void CompletePack(int value,int cost) { for(int v=cost;v<=m;v++) dp[v]=max(dp[v],dp[v-value]+cost); } void MultiPack(int value,int cost,int amount) { if(value*amount>=m) { CompletePack(value,cost); return ; } for(int k=1;k<amount;k*=2) { ZeroOnePack(k*value,k*cost); amount-=k; } ZeroOnePack(amount*value,amount*cost); }
|
二进制优化
假如给了我们价值为2,数量是10的物品,我们应该把10给拆开,首先算出2^0=1,2^1=2,2^2=4,2^3=8。好了那么现在,1+2+4=7<10,1+2+4+8=15>10,所以剩下
的数字应为10-7=3。最后组成10的数字就求得为1,2,4,3。由这4个数字可以表示1-10里的任意一个数,所以这批物品就可以表示为价值为2,4,6,8的四种货物,总价
值依然为20,问题就得到了简化。10,1+2+4+8=15>
二维划为一维数组的正逆序问题
鉴于上面的分析,在01背包中应采取逆序遍历,而在完全背包中应采取正向遍历,原因参考下列博客:
由于该博主已经十分详细的分析了,这里就不在赘述
但上篇博客未牵涉到完全背包问题,所以做如下补充:
想想为什么P01中要按照v=V..0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,
保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加
选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v= 0..V的顺序循环。这就是这个简单的程序为何成立的
道理。
ACM-HDOJ 2546(01背包)
Problem Description
Input
Output
Sample Input
Sample Output
参考AC代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| using namespace std; int cmp(int a,int b) { return a<b; } int main() { int n; while(cin>>n && n!=0) { int price[1050]={0},m,dp[1050]={0}; for(int i=1;i<=n;i++) cin>>price[i]; cin>>m; if(m<5) { cout<<m<<endl; continue; } sort(price+1,price+1+n,cmp); int Max=price[n]; m-=5; for(int i=1;i<n;i++) for(int j=m;j>=price[i];j--) dp[j]=max(dp[j],dp[j-price[i]]+price[i]); cout<<(m-dp[m])+(5-Max)<<endl; } return 0; }
|
思路
典型的01背包问题。但在使用状态方程前应先做如下操作:由于卡里的余额必须要大于5,所以可以做先拿5元买最贵的菜,接着卡里剩余m-5使用01背包问题求出最大价
值(这是逆向思维,若换成正序的情形更容易理解,相当于先买除了最贵以外的菜,确保买完后卡里的钱大于5元而且最少,最接近5元,接着用这5元去买最贵的菜,卡
里剩下的负值就是最小的余额)。
注意事项
虽然题目类型是典型的01背包问题,但是不代表可以直接使用状态方程求解,在经过一系列的操作后才可以使用。如本题在使用状态方程前需先用5元去买最贵的物品。
至于为什么要排序,详见DP专题二————ACM-HDOJ 3466题思路
ACM-HDOJ 1963(完全背包)
Problem Description
Input
Output
Sample Input
Sample Output
参考AC代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| using namespace std; struct node { int cost,reward; }a[20]; int dp[1000]; int main() { int t; cin>>t; while(t--) { int sum,year,n; cin>>sum>>year; cin>>n; for(int i=1;i<=n;i++) { cin>>a[i].cost>>a[i].reward; a[i].cost/=1000; } for(int i=1;i<=year;i++) { int s=sum/1000; memset(dp,0,sizeof(dp)); for(int j=1;j<=n;j++) for(int k=a[j].cost;k<=s;k++) dp[k]=max(dp[k],dp[k-a[j].cost]+a[j].reward); sum+=dp[s]; } cout<<sum<<endl; } return 0; }
|
思路
因为每种物品可以进行多次购买,所以可以看作完全背包问题。但是要注意的是,由于本金可能会很大,所以我们要对背包的大小进行压缩,值得注意的是,题目已经说了本
金与物品的购买价格都是1000的倍数,所以我们可以将他们都除以1000来进行压缩,然后就是一道完全背包模板题了。
注意事项
要对背包的大小进行压缩。
ACM-HDOJ 1059(多重背包)
Problem Description
Input
Output
Sample Input
Sample Output
题目要求
有一堆钻石,有6种不同的价值(1…6),现在告诉你每种的数量,问你能不能把这堆钻石按总的价值平均分开。
参考AC代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| using namespace std; int data[7]; int dp[60001]; int m; void ZeroOnePack(int value,int weight) { for(int v=m;v>=value;v--) dp[v]=max(dp[v],dp[v-value]+weight); } void CompletePack(int value,int weight) { for(int v=value;v<=m;v++) dp[v]=max(dp[v],dp[v-value]+weight); } void MultiPack(int value,int weight,int amount) { if(value*amount>=m) { CompletePack(value,weight); return; } for(int k=1;k<amount;k*=2) { ZeroOnePack(k*value,k*weight); amount-=k; } ZeroOnePack(amount*value,amount*weight); } int main() { int count=1; while(1) { int sum=0; bool mark=true; for(int i=1;i<=6;i++) { cin>>data[i]; sum+=data[i]*i; if(data[i]) mark=false; } if(mark) break; cout<<"Collection #"<<count++<<":"<<endl; if(sum&1) { cout<<"Can't be divided."<<endl; cout<<endl; continue; } m=sum>>1; memset(dp,0,sizeof(dp)); for(int i=1;i<=6;i++) if(data[i]) MultiPack(i,i,data[i]); if(dp[m]==m) cout<<"Can be divided."<<endl; else cout<<"Can't be divided."<<endl; cout<<endl; } return 0; }
|
思路
只要能把钻石总价值的一半放进背包中,就满足题意。首先求出sum,并sum的一半赋给m作为背包最大容量。接着6个位置中每个位置进行判断,若不为0则进入模版进
行判断。01背包和完全背包军用了一维数组形式的状态转移方程,注意01背包中循环是逆序,完全背包是正序即可。多重背包中,如果总价值大于背包的最大容量,那
么就可以等效为完全背包问题。否则进行二进制优化,优化的结果均带入01背包进行计算,最后得出的dp[m]与m进行判断。
注意事项
若总价值为奇数则可以直接剪枝,用sum&1的二进制方法比sum%2的方法更简洁。sum&1为0说明是偶数,为1说明是奇数。同样m=sum/2也可以用sum>>=1来表示,把
二进制数像右移动一位相当于缩小了2^1倍。