根据这几天的学习情况,总结一下对于背包的理解和一些实现方式:
1.大名鼎鼎的0/1背包:这个就不多总结了
2.完全背包:
应该明白,通俗意义上完全背包指的是对于n个价值为v,重量为w的物品,每个物品可以无限次的取(而对于0/1来讲,则是只能取一次)
这怎么处理?
如果按照解01背包时的思路,令 f[i][j]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:
f[i][j]=max(f[i][j],f[i-1][j-k*w[i]+k*v[i]);
但很不幸的是,这种情况大多是超时的
我们用滚动数组优化,具体优化方式如下:
既然01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为01背包问题来解。最简单的想法是,考虑到第 i i i种物品最多选 V/c[i]件,于是可以把第 i种物品转化为 V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。
1 for (int i = 1; i <= n; i++)//大循环 2 for (int j = c[i]; j <= V; j++)//正序啦,注意是正序 3 dp[j] = max([dpj], dp[j - c[i]] + w[i]);//状态以及转移方程
首先想想为什么01背包中要按照 j = V . . . 0 逆序来循环。这是因为要保证第 i次循环中的状态f[i][j]是由状态f[i−1][j−c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果 f[i−1][j−c[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第 i种物品的子结果f[i][j−c[i]],所以就可以并且必须采用 j = 0... V 的顺序循环。
实战:https://www.acwing.com/problem/content/3/
https://www.luogu.com.cn/problem/P1853#sub
以上两个题的参考代码以及注意事项如下:
#includeusing namespace std; int n,v; int va[1010]; int w[1010]; int dp[1010]; int main() { ios::sync_with_stdio(false); cin>>n>>v; for(register int i=1;i<=n;i++) cin>>w[i]>>va[i]; for(register int i=1;i<=n;i++) { for(register int j=w[i];j<=v;j++) { dp[j]=max(dp[j],dp[j-w[i]]+va[i]); } } cout< endl; return 0; }
洛谷:
1 #include2 using namespace std; 3 struct node 4 { 5 int weight; 6 int value; 7 }a[51]; 8 int s; 9 int n; 10 int d; 11 int dp[10000010]; 12 int main(){ 13 cin>>s>>n>>d; 14 for(int i=1;i<=d;i++){ 15 cin>>a[i].weight>>a[i].value; 16 } 17 for(int k=1;k<=n;k++){//最外层是循环刷新s值的,否则s一直为本金 18 for(int i=1;i<=d;i++){//完全背包从这里开始(i,j) 19 for(int j=a[i].weight;j<=s;j++){ 20 dp[j]=max(dp[j],dp[j-a[i].weight]+a[i].value);//完全背包公式 21 } 22 } 23 s=s+dp[s];//刷新s的值 24 } 25 cout<<s; 26 return 0; 27 }
3.多重背包:
多重背包无非是设了限定条件,每个物品最多可以取s[i]件,代码如下,不在多解释
for (int i = 1; i <= N; i++) for (int j = V; j >= w[i]; j--) for (int k = 1; k <= p[i] && k * w[i] <= j; k++) f[j] = max(f[j], f[j - w[i] * k] + v[i] * k);
这里重点要说的是多重背包的二进制优化以及单调队列优化(单调队列优化用的不太熟,就不多说了)
我们来讲讲二进制优化:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为1,2,4,...2^k-1,s[i]-2^k+1;
并且s[i]-2^k+1>0的最大整数。如对于20来讲,可分成1,2,4,8,5,我们要取出7个数,我们只要把4,1,2推出来就好啦,这里面有一个分苹果问题,不在多述。
另外这种方法也能保证对于 0... s[i] 间的每一个整数,均可以用若干个系数的和表示,这个证明可以分 0... 2^k-1, 2^k---s[i]两端分别得出,在时间上是远远优化于原来大循环算法的,具有log运算的优越性
1 for(register int i=1;i<=n;i++) 2 { 3 int num=min(s[i],v/weight[i]);//最多放进取的数目 4 for(register int k=1;num>0;k<<=1)//二进制操作符,不会的话取百度,不多讲 5 { 6 if(k>num) 7 k=num; 8 num-=k; 9 for(register int j=v;j>=k*weight[i];j--)//转化为0/1 10 { 11 dp[j]=max(dp[j],dp[j-k*weight[i]]+k*value[i]); 12 } 13 } 14 }
实战项目:https://www.luogu.com.cn/problem/P1776
#includeusing namespace std; int n,w; int value[1000010]; int weight[100010]; int dp[1000010]; int s[1000010];//上限 int main() { ios::sync_with_stdio(false); cin>>n>>w; for(register int i=1;i<=n;i++) cin>>value[i]>>weight[i]>>s[i]; for(register int i=1;i<=n;i++) { int num=min(s[i],w/weight[i]);//最多可以放多少 for(register int k=1;num>0;k<<=1) { if(k>num) k=num;//如果二进制系数超过了最大值,那就让最大值等于二进制系数 num-=k; //减去消耗的 for(register int j=w;j>=k*weight[i];j--)//纵浪大化中,不喜亦不惧,化0/1背包,如是而已 { dp[j]=max(dp[j],dp[j-k*weight[i]]+k*value[i]); } } } cout<
4.二维背包问题:
二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设第i件物品所需的两种代价分别为c[i]和g[i]。两种代价可付出的最大值(两种背包容量)分别为V和 M。物品的价值为w[i]。
思路:无非多了一个费用,那就开三维数组;
dp[i][j][k]=max(dp[i][j][k],dp[i-1][j-weight[i][k-weight2[i]+value[i]);
1 for (int i = 1; i <= n; i++) 2 for (int j = V; j >= c[i]; j--) 3 for (int k = M; k >= g[i]; k--) 4 f[j][k] = max(f[j][k], f[j - c[i]][k - g[i]] + w[i]);
在源头上也是属于0/1;
实战:https://www.luogu.com.cn/problem/P1507;
https://www.acwing.com/problem/content/8/;
1 #pragma GCC optimize(2)//很普通的O2优化 2 #include3 using namespace std; 4 int n,v,m; 5 int tiji[1010]; 6 int value[1010]; 7 int weight[1010]; 8 int dp[1010][1010];//开三维数组用二维的滚动数组优化 9 int main() 10 { 11 ios::sync_with_stdio(false); 12 cin>>n>>v>>m; 13 for(register int i=1;i<=n;i++) 14 cin>>tiji[i]>>weight[i]>>value[i]; 15 for(register int i=1;i<=n;i++)//化0/1背包问题 16 { 17 for(register int j=v;j>=tiji[i];j--)//依旧是倒叙,原因不多解释 18 { 19 for(register int k=m;k>=weight[i];k--) 20 { 21 dp[j][k]=max(dp[j][k],dp[j-tiji[i]][k-weight[i]]+value[i]);//状态以及转移方程 22 } 23 } 24 } 25 cout< //最后的输出 26 return 0; 27 }
洛谷:
1 #include2 using namespace std; 3 int vmax; 4 int wmax; 5 int n; 6 int dp[1010][1010]; 7 int weight[1010]; 8 int tiji[1010]; 9 int value[1010]; 10 int main() 11 { 12 ios::sync_with_stdio(false); 13 cin>>vmax>>wmax>>n; 14 for(register int i=1;i<=n;i++) 15 cin>>tiji[i]>>weight[i]>>value[i]; 16 for(register int i=1;i<=n;i++) 17 { 18 for(register int j=vmax;j>=tiji[i];j--) 19 { 20 for(register int k=wmax;k>=weight[i];k--) 21 { 22 dp[j][k]=max(dp[j][k],dp[j-tiji[i]][k-weight[i]]+value[i]); 23 } 24 } 25 } 26 cout< endl; 27 return 0; 28 }
代码思路很明确,不多讲了。
明天看情况更新泛化背包以及混合背包以及本讲中没有提及到的其他九讲中的背包。
加油加油,奔向远方,成为百科全书式的人。