硬币问题

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("\n%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("\n");
    }
    
//    for (int i = 1; i < n; i++) {
//        printf("%d  ", result[i]);
//    }
    
    return result[n - 1].minCoin;
}

int main()
{
    printf("\n最少的币数 = %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
posted @ 2020-03-15 13:38  和风细羽  阅读(325)  评论(0编辑  收藏  举报