分硬币问题
分硬币问题是一个很有意思的问题,一般可以用recursive或者dp来解决。Reference里面acm之家的文章分析得很好,下面我自己再重复一边这个过程。这道题也可以联想到其他dp过程。
题目很简单: 给定一堆硬币coins[], 数量管够,硬币有m种不同的面值,求组成 n 元钱有多少种不同的方法。
- 首先我们考虑递归。
- 当n < 0的时候,我们返回0
- 当n = 0的时候,我们返回1,只有一种组合就是所有硬币都取0个
- 当m <= 0的时候,没有可以用的硬币种类了,我们返回0
- 其他情况,我们可以分为两种
- 不使用第m种硬币,那么我们可以递归求解子问题,结果应该是count(coins, m - 1, n)
- 至少使用一次第m种硬币,也是分解为相同子问题,结果是count(coins, m, n - coins[m - 1])
- 接下来我们考虑用dynamic programming来解决,也就是把上面的逻辑转换为记忆化搜索
- 首先我们要建立一个数组dp[][] = new int[n + 1][m]
- 初始化这个数组,把第一行设置为1。因为这里n = 0,所以我们每个硬币都只有一种方法,就是不选择这些硬币,所以第一行都为1
- 当i 从 1 到 dp.length, j从0到m进行遍历的时候,我们依然是分为两种情况进行考虑
- 使用第j种硬币:
- 那么假如当前 i - coins[j] >= 0的话,我们可以使用第j种硬币,相应的值为dp[i - coins[j]][j]。意思是我们减掉这个coins[j]的值,到之前已经保存的i - coins[j]这一行的第j列去看有多少种方法
- 否则我们不能使用这类硬币,值为0
- 不适用第j种硬币:
- 那么假如j > 1的话,我们可以有多少种方法完全决定于同一行内的上一个数据dp[i][j - 1]
- 否则值为0
- 把这两种情况综合一下 x + y, 就是dp[i][j]应该有的值了。
- 接下来我们对上面的dp进行空间上的简化,用滚动数组来代替二维数组:
- 声明一个滚动数组table = new int[n + 1]
- table[0] = 1
- 当i 从 0 到 m, j 从dp[i] 到 n + 1遍历的时候 table[j] += table[j - coins[i]]
- 举个例子,假如 n = 10,我们有1, 2 ,5这三种硬币,那么过程如下
- 初始化 [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
- 使用1 [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
- 使用1和2 [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6]
- 使用1,2和5 [1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 10]
C#:
2D DP
int[] coins = { 1, 2, 5 }; int m = coins.Length; int n = 100; int[,] dp = new int[n + 1, m]; for (int i = 0; i < m; i++) { dp[0, i] = 1; } for (int i = 1; i < n + 1; i++) // 2d dp { for (int j = 0; j < m; j++) { int x = (i - coins[j] >= 0) ? dp[i - coins[j], j] : 0; int y = (j > 0) ? dp[i, j - 1] : 0; dp[i, j] = x + y; } } Console.WriteLine(dp[n, m - 1]);
滚动数组DP
int[] coins = { 1, 2, 5 }; int m = coins.Length; int n = 100; int[] table = new int[n + 1]; table[0] = 1; for (int i = 0; i < m; i++) { for (int j = coins[i]; j < table.Length; j++) { table[j] += table[j - coins[i]]; } } Console.WriteLine(table[n]);
Update: 6-20-2016
To be updated:
1. n元钱有多少种组成方式
2. 最少可以用多少个硬币来组成n元钱
Reference:
http://www.acmerblog.com/dp6-coin-change-4973.html
http://www.geeksforgeeks.org/dynamic-programming-set-7-coin-change/