问题:
假设有m种面值不同的硬币,个个面值存于数组S ={S1,S2,… Sm}中,现在用这些硬币来找钱,各种硬币的使用个数不限。 求对于给定的钱数N,我们最多有几种不同的找钱方式。硬币的顺序并不重要。
例如,对于N = 4,S = {1,2,3},有四种方案:{1,1,1,1},{1,1,2},{2,2},{1, 3}。所以输出应该是4。对于N = 10,S = {2,5, 3,6},有五种解决办法:{2,2,2,2,2},{2,2,3,3},{2,2,6 },{2,3,5}和{5,5}。所以输出应该是5。
1)最优子结构
要算总数的解决方案,我们可以把所有的一整套解决方案在两组 (其实这个方法在组合数学中经常用到,要么包含某个元素要么不包含,用于递推公式等等,)。
1)解决方案不包含 第m种硬币(或Sm)。
2)解决方案包含至少一个 第m种硬币。
让数(S [] , M, N)是该函数来计算解的数目,则它可以表示为计数的总和(S [], M-1, N)和计数(S [],M,N-Sm)。
因此,这个问题具有最优子结构性质的问题。
2) 重叠子问题
下面是一个简单的递归实现硬币找零问题。遵循上面提到的递归结构。
01 |
#include<stdio.h> |
02 |
int count( int S[], int m, int n
) |
03 |
{ |
04 |
//
如果n为0,就找到了一个方案 |
05 |
if (n
== 0) |
06 |
return 1; |
07 |
if (n
< 0) |
08 |
return 0; |
09 |
//
没有硬币可用了,也返回0 |
10 |
if (m
<=0 ) |
11 |
return 0; |
12 |
//
按照上面的递归函数 |
13 |
return count(
S, m - 1, n ) + count( S, m, n-S[m-1] ); |
14 |
} |
15 |
16 |
//
测试 |
17 |
int main() |
18 |
{ |
19 |
int i,
j; |
20 |
int arr[]
= {1, 2, 3}; |
21 |
int m
= sizeof(arr)/sizeof(arr[0]); |
22 |
printf("%d
",
count(arr, m, 4)); |
23 |
getchar(); |
24 |
return 0; |
25 |
} |
应当指出的是,上述函数反复计算相同的子问题。见下面的递归树为S = {1,2,3},且n = 5。
的函数C({1},3)被调用两次。如果我们绘制完整的树,那么我们可以看到,有许多子问题被多次调用。
01 |
C()
--> count() |
02 |
C({1,2,3},
5) |
03 |
/
\ |
04 |
/
\ |
05 |
C({1,2,3},
2) C({1,2}, 5) |
06 |
/
\ / \ |
07 |
/
\ / \ |
08 |
C({1,2,3},
-1) C({1,2}, 2) C({1,2}, 3) C({1}, 5) |
09 |
/
\ / \ / \ |
10 |
/
\ / \ / \ |
11 |
C({1,2},0)
C({1},2) C({1,2},1) C({1},3) C({1}, 4) C({}, 5) |
12 |
/
\ / \ / \ / \ |
13 |
/
\ / \ / \ / \ |
14 |
.
. . . . . C({1}, 3) C({}, 4) |
15 |
/
\ |
16 |
/
\ |
17 |
.
. |
所以,硬币找零问题具有符合动态规划的两个重要属性。像其他典型的动态规划(DP)的问题,可通过自下而上的方式打表,存储相同的子问题。当然上面的递归程序也可以改写成记忆化存储的方式来提高效率。
下面是动态规划的程序:
01 |
#include<stdio.h> |
02 |
03 |
int count( int S[], int m, int n
) |
04 |
{ |
05 |
int i,
j, x, y; |
06 |
07 |
//
通过自下而上的方式打表我们需要n+1行 |
08 |
//
最基本的情况是n=0 |
09 |
int table[n+1][m]; |
10 |
11 |
//
初始化n=0的情况 (参考上面的递归程序) |
12 |
for (i=0;
i<m; i++) |
13 |
table[0][i]
= 1; |
14 |
15 |
for (i
= 1; i < n+1; i++) |
16 |
{ |
17 |
for (j
= 0; j < m; j++) |
18 |
{ |
19 |
//
包括 S[j] 的方案数 |
20 |
x
= (i-S[j] >= 0)? table[i - S[j]][j]: 0; |
21 |
22 |
//
不包括 S[j] 的方案数 |
23 |
y
= (j >= 1)? table[i][j-1]: 0; |
24 |
25 |
table[i][j]
= x + y; |
26 |
} |
27 |
} |
28 |
return table[n][m-1]; |
29 |
} |
30 |
31 |
//
测试 |
32 |
int main() |
33 |
{ |
34 |
int arr[]
= {1, 2, 3}; |
35 |
int m
= sizeof(arr)/sizeof(arr[0]); |
36 |
int n
= 4; |
37 |
printf("
%d ",
count(arr, m, n)); |
38 |
return 0; |
39 |
} |
时间复杂度:O(mn)
以下为上面程序的优化版本。这里所需要的辅助空间为O(n)。因为我们在打表时,本行只和上一行有关,类似01背包问题。
01 |
int count( int S[], int m, int n
) |
02 |
{ |
03 |
int table[n+1]; |
04 |
memset(table,
0, sizeof(table)); |
05 |
//初始化基本情况 |
06 |
table[0]
= 1; |
07 |
08 |
for(int i=0;
i<m; i++) |
09 |
for(int j=S[i];
j<=n; j++) |
10 |
table[j]
+= table[j-S[i]]; |
11 |
12 |
return table[n]; |
13 |
} |
参考:http://www.geeksforgeeks.org/dynamic-programming-set-7-coin-change/
http://www.algorithmist.com/index.php/Coin_Change
776

被折叠的 条评论
为什么被折叠?



