
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 的总价值需要的最少硬币数量。
- 当 i = 0 时,很显然知道 d(0) = 0。
- 当 i = 1 时,因为有 1 元的硬币,所以直接在第 1 步的基础上,加上 1 个 1 元硬币,得出 d(1) = d(0) + 1。
- 当 i = 2 时,因为并没有 2 元的硬币,所以在第 2 步的基础上,加上 1 个 1 元硬币,得出 d(2) = d(1) + 1。
- 当 i = 3 时,需要 3 个 1 元硬币或者 1 个 3 元硬币,d(3) = min{ d(2)+1, d(3-3)+1 };
- ...
- 抽离出来 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