zoukankan      html  css  js  c++  java
  • 硬币问题

    1、问题描述

    假设有 1 元、3 元、5 元的硬币无限个,现在需要凑出 11 元,问如何组合才能使硬币的数量最少?

    2、算法分析

    有最小单位 1 的情况下,可以使用贪心算法:

    NSInteger count = m / 5;
    NSInteger mol   = m % 5;
       
    if(mol/3 > 0) {
         count++;
         mol %= 3;
    }
       
    count += mol;
    

    但当硬币的种类改变,并且需要凑出的总价值变大时,很难靠简单的计算得出结果。贪心算法可以在一定的程度上得出较优解,但不是每次都能得出最优解。

    这里运用动态规划的思路解决该问题。动态规划中有三个重要的元素:最优子结构、边界、状态转移公式。按照一般思路,先从最基本的情况来一步一步地推导。

    注意:动态规划的策略在于当前的硬币(或其他物品)是否能算进去

    先假设一个函数 d(i) 来表示需要凑出 i 的总价值需要的最少硬币数量。

    1.  当 i = 0 时,很显然知道 d(0) = 0。
    2.  当 i = 1 时,因为有 1 元的硬币,所以直接在第 1 步的基础上,加上 1 个 1 元硬币,得出 d(1) = d(0) + 1。
    3.  当 i = 2 时,因为并没有 2 元的硬币,所以在第 2 步的基础上,加上 1 个 1 元硬币,得出 d(2) = d(1) + 1。
    4.  当 i = 3 时,需要 3 个 1 元硬币或者 1 个 3 元硬币,d(3) = min{ d(2)+1, d(3-3)+1 };
    5.   ...
    6.  抽离出来 d(i) = min{ d(i-1)+1, d(i-vj)+1 },其中 i - vj >= 0,vj 表示第 j 个硬币的面值。

    这里 d(i-1)+1 和 d(i-vj)+1 是 d(i) 的最优子结构;d(0) = 0 是边界;d(i) = min{ d(i-1)+1, d(i-vj)+1 } 是状态转移公式。其实我们根据边界 + 状态转移公式就能得到最终动态规划的结果。

    3、算法实现

    #include <stdio.h>
    #include <stdlib.h>
    
    #define Coins 3
    
    int dp(int n)
    {
        // min 数组包含 d(0)~d(n),所以数组长度是 n+1
        n++;
        // 初始化数组
        int* min = (int*)calloc(n, sizeof(int));
        
        // 可选硬币种类
        int v[Coins] = { 1, 3, 5 };
        
        for (int i = 1; i < n; i++) {
            
            min[i] = min[i-1] + 1;
            
            for (int j = 0; j < Coins; j++) {
                
                // 装不下
                if (v[j] > i) {
                    break;
                }
                
                // 装得下
                if (min[i - v[j]] < min[i - 1]) {
                    min[i] = min[i - v[j]] + 1;
                }
            }
        }
        
        for (int i = 0; i < n; i++) {
            printf("%d  ", min[i]);
        }
        
        return min[n - 1];
    }
    
    int main()
    {
        printf("
    %d", dp(101));
        
        return 0;
    }
    

    4、拓展

    上面的问题中包含了最小单位 1 元的硬币,所以每次 i 增加时,都能 min[i] = min[i - 1] + 1(+1 是用了 1 元硬币),但如果硬币为 2 元、3 元、5 元呢?应该如何求出 11 元呢?

    来推算下:

    ①、n = 1,不存在 1 元硬币,且 2、3、5 > 1,所以 f(1) = 0;
    
    ②、n = 2,存在 2 元硬币,所以 f(2) = 1;
    
    ③、n = 3,存在 3 元硬币,所以 f(3) = 1;
    
    ④、n = 4,不存在 4 元硬币,而 2 和3 < 4,5 > 4,其中
    
        f(4-3) = f(1) = 0 说明在去除 3 元的情况下,不能获得剩下的 1 元;
    
        f(4-2) = f(2) = 1 说明在去除2 元的情况下,可以获得剩下的2 元,f(4) = f(2) + 1 = 2;
    
        结合上面两种情况 f(4) = MIN{ f(4-2) + 1 }
    
    ⑤、n = 5,存在 5 元硬币,所以 f(5) = 1;
    
    ⑥、n = 6,不存在 6元硬币,而 2、3、5 < 6,其中 
    
        f(6-5) = f(1) = 0 说明在去除 5 元的情况下,不能获得剩下的 1 元;
    
        f(6-3) = f(3) = 1 说明在去除 3 元的情况下,可以获得剩下的 3 元,f(6) = f(6-3) + 1 = 2;
    
        f(6-2) = f(4) = 2 说明在去除 2 元的情况下,可以获得剩下的 3 元,f(6) = f(6-4) + 1 = 3;
    
        结合上面三种情况 f(6) = MIN{ f(6-3) + 1, f(6-2) + 1 }
    

    【状态】是 f(n)

    【边界】是 n = 2、3、5 时只有一种选择

    【状态转移方程】是 f(n) = MIN{  f(n - ci) +1 }, 其中 n 表示当前的总额,ci 表示金币数额。

    注意:因为是取最小值,所以是无法获得的总额时,如 f(1),应该让 f(1)等于很大的值,这样就可以将它剔除出去。

    下面的代码为了直观每次选币的过程,增加了结构体、打印代码,不需要时可以自行删除。

    #include <stdio.h>
    #include <stdlib.h>
    
    #define Coins     3
    #define MIN(a, b) (a) < (b) ? (a) : (b)
    
    typedef struct CoinLog {
        int minCoin;   // 最少的硬币数
        int coin[100]; // 所选硬币
    } CoinLog;
    
    int dp(int n)
    {
        n++;  // result 数组包含 d(0)~d(n),所以数组长度是 n+1
        
        // 初始化数组
    //    int* result = (int*)malloc(sizeof(int) * n);
    //    for (int i = 0; i < n; i++) {
    //        result[i] = n;
    //    }
        CoinLog* result = (CoinLog *)malloc(sizeof(CoinLog) * n);
        for (int i = 0; i < n; i++) {
            CoinLog log = { n, {0} };
            result[i] = log;
        }
        
        // 硬币种类
        int v[Coins] = { 2, 3, 5 };
        
        for (int i = 1; i < n; i++) {
            
            printf("%3d =", i);
            for (int j = 0; j < Coins; j++) {
                
                // 硬币正好
                if (v[j] == i) {
                    result[i].minCoin = 1;
                    result[i].coin[0] = v[j];
                }
                // 硬币太大
                else if (v[j] > i) {
                    
                }
                // 循环 Coins,找出最少的币数
                else if (result[i - v[j]].minCoin < result[i].minCoin) {
                    result[i].minCoin = result[i - v[j]].minCoin + 1;
                    
                    int k = 0;
                    for (; k < result[i - v[j]].minCoin; k++) {
                        result[i].coin[k] = result[i - v[j]].coin[k];
                    }
                    result[i].coin[k] = v[j];
                }
            }
            
            if (result[i].minCoin < n) {
                // 显示每次怎么找的
                for (int k = 0; k < result[i].minCoin; k++) {
                    printf("%3d  ", result[i].coin[k]);
                }
            }
            printf("
    ");
        }
        
    //    for (int i = 1; i < n; i++) {
    //        printf("%d  ", result[i]);
    //    }
        
        return result[n - 1].minCoin;
    }
    
    int main()
    {
        printf("
    最少的币数 = %d", dp(21));
        
        return 0;
    }
      1 =
      2 =  2  
      3 =  3  
      4 =  2    2  
      5 =  5  
      6 =  3    3  
      7 =  2    5  
      8 =  3    5  
      9 =  2    2    5  
     10 =  5    5  
     11 =  3    3    5  
     12 =  2    5    5  
     13 =  3    5    5  
     14 =  2    2    5    5  
     15 =  5    5    5  
     16 =  3    3    5    5  
     17 =  2    5    5    5  
     18 =  3    5    5    5  
     19 =  2    2    5    5    5  
     20 =  5    5    5    5  
     21 =  3    3    5    5    5 
      
    最少的币数 = 5
    
  • 相关阅读:
    [SDOI2015] 序列统计
    [BZOJ3514] Codechef MARCH14 GERALD07加强版
    [CF1082E] Increasing Frequency
    [CF1093G] Multidimensional Queries
    [HNOI2013] 切糕
    [HEOI2017] 寿司餐厅 + 最大权闭合子图的总结
    [BZOJ3771] Triple
    [HEOI2016] 字符串
    [总结] 后缀数组学习笔记
    [Luogu 3613] 睡觉困难综合征
  • 原文地址:https://www.cnblogs.com/dins/p/coin-problem.html
Copyright © 2011-2022 走看看