一、题目描述
美国的硬币按照面值1, 5, 10, 25, 50来铸造(单位为美分)。现在考虑按照面值{d1,…, dk}(单位为分)来铸造硬币的某个国家。我们想要寻找一个算法能用最少数目的该国硬币来找开n分钱。给出一种高效算法能确定出用面值集合{d1,…, dk}找开n分钱所需的最少硬币数。
———题目来源:《算法设计指南》
示例:
输入:coins = [2,3,5] total = 12
输出:3
解释:2 + 5 + 5最少需要3枚硬币找开12分钱
二、解题思路(朴素版本)
其实本题就是完全背包问题。
1. 定义状态
设dp[i][j]表示使用前i个面值不同的硬币找出零钱j的所用的最少硬币数。
2. 定义状态转移方程
d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j − k × d i ] + k ) dp[i][j] = min(dp[i - 1][j - k \times d_i] + k) dp[i][j]=min(dp[i−1][j−k×di]+k) ,其中 0 ≤ k ≤ ⌊ j d i ⌋ 0 \leq k \leq \lfloor \frac{j}{d_i} \rfloor 0≤k≤⌊dij⌋ 。这里解释一下 d p [ i − 1 ] dp[i-1] dp[i−1]表示使用前 i − 1 i-1 i−1个面值不同的硬币,其中第 i i i个硬币可能使用0个或多个,上限为 ⌊ j d i ⌋ \lfloor \frac{j}{d_i} \rfloor ⌊dij⌋ 个,下限为0个。
3. 初始化
dp[0][j]表示不使用硬币找出j的最小零钱数,初始化为 Integer.MAX_VALUE - total,注意这里不能初始化为Integer.MAX_VALUE,因为后面用到这个值计算会溢出变为最小值导致程序运行结果不正确。
dp[i][0]表示使用前i个面值不同硬币找出0美分所需硬币数,很显然dp[i][0]初始化为0即可。
三、代码实现
/**
* 第六题 找零钱需要最少的硬币数
*
* @author hh
*/
public class Solution {
/**
* 求找零钱最小硬币数
*
* @param coins 硬币面值集合
* @param total 找零钱总数
* @return 最少硬币数
*/
public int minCoins(int[] coins, int total) {
int[][] dp = new int[coins.length + 1][total + 1];
//初始化行
for (int i = 1; i <= total; i++) {
//注意下这里的值不能初始化为Integer.MAX_VALUE
dp[0][i] = Integer.MAX_VALUE - total;
}
//初始化列
for (int i = 0; i <= coins.length; i++) {
dp[i][0] = 0;
}
for (int i = 1; i <= coins.length; i++) {
for (int j = 1; j <= total; j++) {
dp[i][j] = Integer.MAX_VALUE;
for (int k = 0; k <= j / coins[i - 1]; k++) {
dp[i][j] = Math.min(dp[i][j],dp[i - 1][j - k * coins[i - 1]] + k );
}
}
}
return dp[coins.length][total];
}
public static void main(String[] args) {
int[] coins = new int[]{2, 3, 5};
int total = 12;
Solution solution = new Solution();
System.out.println(solution.minCoins(coins, total));
}
}
四、优化
对于解题思路中的k其实是没必要计算的,这样就减少了一个循环。状态方程变为:
d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − d i ] + 1 ) dp[i][j] = min(dp[i - 1][j],dp[i][j - d_i] + 1) dp[i][j]=min(dp[i−1][j],dp[i][j−di]+1)
第一个式子表示的含义是不使用第i种硬币凑成零钱j的硬币数。第二个式子表示最少有一枚硬币i凑成零钱总数j的硬币数。
代码如下所示:
public class Solution2 {
/**
* 求找零钱最小硬币数
*
* @param coins 硬币面值集合
* @param total 找零钱总数
* @return 最少硬币数
*/
public int minCoins(int[] coins, int total) {
int[][] dp = new int[coins.length + 1][total + 1];
//初始化行
for (int i = 1; i <= total; i++) {
//注意下这里的值不能初始化为Integer.MAX_VALUE
dp[0][i] = Integer.MAX_VALUE - total;
}
//初始化列
for (int i = 0; i <= coins.length; i++) {
dp[i][0] = 0;
}
for (int i = 1; i <= coins.length; i++) {
for (int j = 1; j <= total; j++) {
if(j < coins[i-1]){
dp[i][j] = dp[i-1][j];
} else{
dp[i][j] = Math.min(dp[i-1][j],dp[i][j - coins[i-1]] + 1);
}
}
}
return dp[coins.length][total];
}
public static void main(String[] args) {
int[] coins = new int[]{2, 3, 5};
int total = 12;
Solution2 solution = new Solution2();
System.out.println(solution.minCoins(coins, total));
}
}
上面是对时间复杂度进行优化,下面对空间进行优化,考虑到dp[i][j]只跟dp[i-1]和dp[i]有关,我们可以把二维降成一维,代码如下所示:
public class Solution3 {
/**
* 求找零钱最小硬币数
*
* @param coins 硬币面值集合
* @param total 找零钱总数
* @return 最少硬币数
*/
public int minCoins(int[] coins, int total) {
int[] dp = new int[total + 1];
//注意下这里的值不能初始化为Integer.MAX_VALUE,因为相加会出现整数溢出
Arrays.fill(dp,Integer.MAX_VALUE - total);
dp[0] = 0;
for (int i = 1; i <= coins.length; i++) {
for (int j = coins[i-1]; j <= total; j++) {
dp[j] = Math.min(dp[j], dp[j - coins[i - 1]] + 1);
}
}
return dp[total];
}
public static void main(String[] args) {
int[] coins = new int[]{2, 3, 5};
int total = 12;
Solution3 solution = new Solution3();
System.out.println(solution.minCoins(coins, total));
}
}