参考资料:
[1]:https://www.cnblogs.com/jbelial/articles/2116074.html
[2]:https://www.luogu.org/problemnew/solution/P1616
题意:
有一个小猪存钱罐,里面有各式各样的硬币,每种硬币有不同的面值和重量。
现测量出存钱罐初始的重量(E)和存满钱时的重量(F),问在这N种硬币中,如何选取,使得选取的硬币的总重量恰好等于(F-E),且总价值最小。
如果可以找到,输出"The minimum amount of money in the piggy-bank is num".(num : 总和最小的面值)
如果不能通过组合使得硬币总重量恰好等于(F-E),则输出"This is impossible."
分析:
乍一看,和“01”背包很想,所不同的是,在“01”背包中,第 i 种物品只有两种选择,拿或者不拿;
而此题,第 i 种面值的硬币可不止有拿与不拿这两种选择,而是有拿0,1,2,.....,k个,共(k+1)种选择,其中k满足 k*w[i] <= (F-E);
这种每种物品都有无限件可用的问题,称为“完全背包”问题。
题解:
1.完全背包转“01”背包
思路:将一种物品拆成多件物品。
考虑到第 i 种物品最多选 (F-E) / w[ i ] 件;
于是可以把第 i 种物品转化为 (F-E) / w[ i ] 件费用及价值均不变的物品,然后求解这个01背包问题。
如果开个二维的dp数组,指定不可行,具体为什么,请自行思考;
提示:假设每种硬币都可拆成 k 个(k 最大可达 10000),N种硬币则可拆成 N*k 个(N最大为500),所以最坏的情况是可拆成 N*k = 5e6 个物品。
比较好用的方法就是使用滚动数组优化空间。
更高效的转化方法是:把第 i 种物品拆成重量为 w[i]×2k、价值为 p[i]×2k 的若干件物品,其中k满足 w[i]×2k < (F-E)。
这是二进制的思想,因为不管最优策略选几件第 i 种物品,总可以表示成若干个 2k 件物品的和。
这样把每种硬币拆成 log2( (F-E) / w[i]) ) 件物品,是一个很大的改进。
AC代码:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define ll long long 4 #define INF 0x3f3f3f3f 5 const int maxn=500+50; 6 7 int n; 8 int e,f; 9 int p[maxn],w[maxn]; 10 int dp[10010]; 11 12 void Solve() 13 { 14 int v=f-e; 15 for(int j=1;j <= v;++j) 16 dp[j]=INF; 17 dp[0]=0; 18 19 for(int i=1;i <= n;++i) 20 { 21 for(int k=0,cur=(1<<k)*w[i];cur <= v;++k)///第i个物品选2^k个 22 { 23 for(int j=v;j >= cur;--j) 24 dp[j]=min(dp[j],dp[j-cur]+(1<<k)*p[i]); 25 26 cur <<= 1; 27 } 28 } 29 if(dp[v] < INF) 30 printf("The minimum amount of money in the piggy-bank is %d.\n",dp[v]); 31 else 32 printf("This is impossible.\n"); 33 } 34 int main() 35 { 36 int test; 37 scanf("%d",&test); 38 while(test--) 39 { 40 scanf("%d%d",&e,&f); 41 scanf("%d",&n); 42 for(int i=1;i <= n;++i) 43 scanf("%d%d",p+i,w+i); 44 Solve(); 45 } 46 return 0; 47 }
2.我们有更优的O(VN)的算法
定义dp[ i ][ j ] : 前 i 件物品恰好组成重量 j 的最小面值;
第 i 件物品的状态转移方程为:
1 for(int j=w[i];j <= v;++j) 2 dp[j]=min(dp[j],dp[j-w[i]]+p[i]);
完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第 i 种物品”这种策略时,
却正需要一个可能已选入第 i 种物品的子结果 dp[ i ][ v-w[i] ],所以就可以并且必须采用 j = w[i]..v 的顺序循环。
以上思路摘抄自[1]%%%%%%%%%%%%%%%
AC代码:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define ll long long 4 #define INF 0x3f3f3f3f 5 const int maxn=500+50; 6 7 int n; 8 int e,f; 9 int p[maxn],w[maxn]; 10 int dp[10010]; 11 12 void Solve() 13 { 14 int v=f-e; 15 for(int j=1;j <= v;++j) 16 dp[j]=INF; 17 dp[0]=0; 18 19 for(int i=1;i <= n;++i) 20 for(int j=w[i];j <= v;++j) 21 dp[j]=min(dp[j],dp[j-w[i]]+p[i]); 22 23 if(dp[v] < INF) 24 printf("The minimum amount of money in the piggy-bank is %d.\n",dp[v]); 25 else 26 printf("This is impossible.\n"); 27 } 28 int main() 29 { 30 int test; 31 scanf("%d",&test); 32 while(test--) 33 { 34 scanf("%d%d",&e,&f); 35 scanf("%d",&n); 36 for(int i=1;i <= n;++i) 37 scanf("%d%d",p+i,w+i); 38 Solve(); 39 } 40 return 0; 41 }