最少货币流通, 硬币找钱问题

硬币找钱问题poj3260

题解摘自挑战程序设计竞赛

双端队列 

流通分为付钱和找钱两个过程,定义两个过程分别需要的硬币数为如下dp数组:


  
  
  1. int dp_change[MAX_T + MAX_V * MAX_V];   // dp_change[i] := 商店找钱金额为i时最少硬币数
  2. int dp_pay[MAX_T + MAX_V * MAX_V];      // dp_pay[i] := 顾客付钱金额为i时最少硬币数

那么最终答案为两个硬币数之和的最小值:


  
  
  1. for (int i = max_v * max_v; i >= 0; --i)
  2. {
  3.     ans = min(ans, dp_change[i] + dp_pay[+ i]);
  4. }

上面付出了T+i金额的钱,收回了i金额的钱,于是实际上就买到了价格T的商品。

问题大的框架就是这样,再来看怎么求解两个子过程的解。

付钱阶段

付钱阶段,每个硬币的携带量有限。将硬币视作物品,硬币的价格作为物品的重量,硬币的个数1作为物品的价值,则给定某个T作为背包容量W,问题转化为“物品价值总和最小”的多重背包问题。

多重背包问题可以用书上的双端队列,也可以二进制分解转化为01背包问题,O(nWlog m)快速求解。


  
  
  1. // 多重背包转化为二进制的01背包
  2. void dp_multiple_pack(int n, int W)
  3. {
  4.     memset(dp_pay, 0x3f, (+ 1) * sizeof(int));
  5.     dp_pay[0] = 0;
  6.     for (int i = 0; i < n; ++i)
  7.     {
  8.         int num = C[i];
  9.         for (int k = 1; num > 0; k <<= 1)
  10.         {
  11.             int mul = min(k, num);
  12.             for (int j = W; j >= V[i] * mul; --j)
  13.             {
  14.                 dp_pay[j] = min(dp_pay[j], dp_pay[- V[i] * mul] + mul);   // 价值为1
  15.             }
  16.             num -= mul;
  17.         }
  18.     }
  19. }

找钱阶段

找钱阶段,硬币数量不限,在类似的思路下直接视作完全背包问题。


  
  
  1. // 完全背包
  2. void dp_complete_pack(int n, int W)
  3. {
  4.     memset(dp_change, 0x3f, (+ 1) * sizeof(int));
  5.     dp_change[0] = 0;
  6.     for (int i = 0; i < n; ++i)
  7.     {
  8.         for (int j = V[i]; j <= W; ++j)
  9.         {
  10.             dp_change[j] = min(dp_change[j], dp_change[- V[i]] + 1);  // "价值总和"最小
  11.         }
  12.     }
  13. }

鸽笼原理

题目的难点其实在于背包容量W的确定,W的意义为最优方案金额的最大值(上界)。

上述定义中频繁出现

 
 
  1. MAX_T + MAX_V * MAX_V

意味着,要凑足(大于等于)价格T的商品且硬币数最少,最多只能多给max_v * max_v的金额(其中max_v为硬币的最大面值),称此上界为W。为什么会有这么紧的上界呢,假设存在一种最优支付方案,给了多于t + max_v * max_v的钱,那么商店就会找回多于max_v * max_v的钱,这些硬币的个数大于max_v。设这些硬币的面值分别为a_i,根据鸽笼原理的应用,硬币序列中存在至少两个子序列,这两个子序列的和分别都能被max_v整除。如果我们直接用长度更小的那个子序列换算为面值为max_v的硬币某整数个,再去替换母序列就能用更少的硬币买到商品,形成矛盾。

关于整除更详细的证明如下,尝试构造这样的子序列。长度为max_v+x的母序列至少可以找两个不同的长度为max_v的子序列出来,按照《组合数学》中的证明:

组合数学 原书第5版-冯速等译.png

hankcs.com 2017-01-15 下午11.46.35.png

它们存在子序列可以被max_v整除。

完整代码


  
  
  1. #include <iostream>
  2.  
  3. using namespace std;
  4. const int MAX_T = 10000 + 4;
  5. const int MAX_N = 100 + 2;
  6. const int MAX_V = 120 + 1;
  7. const int INF = 0x3f3f3f3f;
  8.  
  9. int N, T;
  10. int V[MAX_N], C[MAX_N];     // 面值和携带个数
  11. int max_v;                  // 最大面值
  12. int dp_change[MAX_T + MAX_V * MAX_V];   // dp_change[i] := 商店找钱金额为i时最少硬币数
  13. int dp_pay[MAX_T + MAX_V * MAX_V];      // dp_pay[i] := 顾客付钱金额为i时最少硬币数
  14.  
  15. // 完全背包
  16. void dp_complete_pack(int n, int W)
  17. {
  18.     memset(dp_change, 0x3f, (+ 1) * sizeof(int));
  19.     dp_change[0] = 0;
  20.     for (int i = 0; i < n; ++i)
  21.     {
  22.         for (int j = V[i]; j <= W; ++j)
  23.         {
  24.             dp_change[j] = min(dp_change[j], dp_change[- V[i]] + 1);  // "价值总和"最小
  25.         }
  26.     }
  27. }
  28.  
  29. // 多重背包转化为二进制的01背包
  30. void dp_multiple_pack(int n, int W)
  31. {
  32.     memset(dp_pay, 0x3f, (+ 1) * sizeof(int));
  33.     dp_pay[0] = 0;
  34.     for (int i = 0; i < n; ++i)
  35.     {
  36.         int num = C[i];
  37.         for (int k = 1; num > 0; k <<= 1)
  38.         {
  39.             int mul = min(k, num);
  40.             for (int j = W; j >= V[i] * mul; --j)
  41.             {
  42.                 dp_pay[j] = min(dp_pay[j], dp_pay[- V[i] * mul] + mul);   // 价值为1
  43.             }
  44.             num -= mul;
  45.         }
  46.     }
  47. }
  48.  
  49. void solve()
  50. {
  51.     dp_multiple_pack(N, T + max_v * max_v);     // 付钱
  52.     dp_complete_pack(N, T + max_v * max_v);     // 找钱
  53.     int ans = INF;
  54.     for (int i = max_v * max_v; i >= 0; --i)
  55.     {
  56.         ans = min(ans, dp_change[i] + dp_pay[+ i]);
  57.     }
  58.     if (ans == INF)
  59.     {
  60.         ans = -1;
  61.     }
  62.     printf("%d\n", ans);
  63. }
  64.  
  65. int main()
  66. {
  67. #ifndef ONLINE_JUDGE
  68.     freopen("in.txt", "r", stdin);
  69. #endif
  70.     scanf("%d%d", &N, &T);
  71.     for (int i = 0; i < N; ++i)
  72.     {
  73.         scanf("%d", &V[i]);
  74.         max_v = max(max_v, V[i]);
  75.     }
  76.     for (int i = 0; i < N; ++i)
  77.     {
  78.         scanf("%d", &C[i]);
  79.     }
  80.     solve();
  81. #ifndef ONLINE_JUDGE
  82.     fclose(stdin);
  83. #endif
  84.     return 0;
  85. }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值