题目
You are given coins of different denominations and a total amount of money amount. Write a function to compute the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.
Example 1:
Input: coins = [1, 2, 5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1
我的想法
这道题不能简单的直接用贪心算法解决,比如amount = 7,coins = [1,3,4,5]
。如果按照贪心算法,则得到的解为[5,1,1]
而实际上的解应该是[3,4]
但通过DP可以通过前项的值来找寻组成当前amount的最小coins解法
结果对的,但是coins = [370,417,408,156,143,434,168,83,177,280,117], amount = 9953
时超时。。。
class Solution {
public int coinChange(int[] coins, int amount) {
if(amount == 0) return 0;
Arrays.sort(coins);
if(coins == null || coins.length == 0 || coins[0] > amount) return -1;
int[] dp = new int[amount+1];
Arrays.fill(dp, -1);
for(int i = 0; i < coins.length; i++) {
if(coins[i] > amount) break;
dp[coins[i]] = 1;
}
for(int i = coins[0]; i <= amount; i++) {
if(dp[i] == 1) continue;
for(int j = i - 1; j >= i/2 && j > 0; j--) {
if(dp[j] == -1 || dp[i-j] == -1) continue;
dp[i] = Math.min(dp[j] + dp[i-j], dp[i]);
if(dp[i] == -1) dp[i] = dp[j] + dp[i-j];
}
}
return dp[amount];
}
}
//不知为何,这样写就不超时了,虽然速度依旧很慢
//其实求dp[i]没有必要把dp[i - 1]、dp[i - 2]、dp[i - 3]...的情况都求出来。因为dp[i]只能由前数加上现有的面额得到
class Solution {
public int coinChange(int[] coins, int amount) {
if(amount == 0) return 0;
Arrays.sort(coins);
if(coins == null || coins.length == 0 || coins[0] > amount) return -1;
int[] dp = new int[amount+1];
Arrays.fill(dp, amount + 1);
for(int i = 0; i < coins.length; i++) {
if(coins[i] > amount) break;
dp[coins[i]] = 1;
}
for(int i = coins[0]; i <= amount; i++) {
if(dp[i] == 1) continue;
for(int j = i - 1; j >= i/2 && j > 0; j--) {
dp[i] = Math.min(dp[j] + dp[i-j], dp[i]);
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
2019.12.2update:
首先想到的是用hashmap来存dp。后来发现其实没必要,不过改用数组有很多corner case要考虑
class Solution {
public int coinChange(int[] coins, int amount) {
if(amount == 0) {
return 0;
}
if(coins.length == 0) {
return -1;
}
HashMap<Integer, Integer> dp = new HashMap<>();
for(int coin : coins) {
dp.put(coin, 1);
}
for(int i = 1; i <= amount; i++) {
if(dp.containsKey(i)) {
continue;
}
for(int coin : coins) {
if(dp.containsKey(i - coin)) {
int count = dp.get(i - coin) + 1;
if(dp.containsKey(i)) {
if(dp.get(i) > count) {
dp.put(i, count);
}
} else {
dp.put(i, count);
}
}
}
}
return dp.containsKey(amount) ? dp.get(amount) : -1;
}
}
class Solution {
public int coinChange(int[] coins, int amount) {
if(amount == 0) {
return 0;
}
Arrays.sort(coins);
if(coins.length == 0 || coins[0] > amount) {
return -1;
}
int[] dp = new int[amount + 1];
Arrays.fill(dp, -1);
for(int coin : coins) {
if(coin > amount) {
continue;
}
dp[coin] = 1;
}
for(int i = 1; i <= amount; i++) {
if(dp[i] != -1) {
continue;
}
for(int coin : coins) {
if(coin > amount) {
continue;
}
if(i - coin > 0 && dp[i - coin] != -1) {
int count = dp[i - coin] + 1;
if(dp[i] != -1) {
if(dp[i] > count) {
dp[i] = count;
}
} else {
dp[i] = count;
}
}
}
}
return dp[amount];
}
}
解答
自顶向下的DP与Recursion with Memoization有什么区别?
以下为个人理解,待求证:
DP是一种利用前值解决当前问题的技术
Memoization:设置一个数组,当需要子问题的解时,先去这个数组中查找。如果此问题之前已经求过解,那么就直接返回该值,如果此问题之前并未求过解,那么就计算该值并把结果放入数组中,以备后用。
Tabulation:用一个表格存放子问题的答案,然后查表获得父问题需要的所有信息去解决父问题,解决后也填在表中,直至把表填满。
自底向上,是从小到大,把所有子问题都解决了再利用子问题的解求解父问题,是个累加过程,用Iteration。即Tabulation
自顶向下,是从大到小。这意味着,求解当前问题,需要先求解其子问题,若该子问题已求解过,则直接使用其解;若子问题还没求解,则先求解子问题并存储其值,再返回来解决父问题,因此一般为递归。即Memoization
因此可以理解为Memoization是实现DP的一种方法
leetcode solution 1: DP - Top down
数额确定其最小组合方式就已经确定,因此可以利用dp数组将已经求解的数额进行存储。而改变当前数额的方式只有加上coins数组中提供的面额
因此,当前数额rem = coins[i] + (rem - coins[i])
,其中coins[i]的组成方式只有1种,(rem - coins[i])
的组成方式为dp[rem - coins[i]]
种。由此可得dp[rem] = dp[rem - coins[i]] + 1
class Solution {
public int coinChange(int[] coins, int amount) {
if (amount < 1) return 0;
return coinHelper(coins, amount, new int[amount + 1]);
}
private int coinHelper(int[] coins, int rem, int[] memo) {
if(rem < 0) return -1;
if(rem == 0) return 0;
//注意这里不能写成>0,==0则说明这个数还没有判断过,-1表示没有解
//如果写成>0会重复遍历 -1没有解的情况
if(memo[rem] != 0) return memo[rem];
int min = Integer.MAX_VALUE;
for(int coin : coins) {
int prev = coinHelper(coins, rem - coin, memo);
if(prev + 1 > 0 && prev + 1 < min) {
min = prev + 1;
}
}
memo[rem] = (min == Integer.MAX_VALUE) ? -1 : min;
return memo[rem];
}
}
leetcode solution 2: DP - Bottom up
感觉这个才是正经的DP算法,比Memoization要快一些。
public class Solution {
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[amount + 1];
Arrays.fill(dp, max);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
//这里只需要遍历面额情况,dp[i]只能由
//前数dp[i - coins[j]]加上现有的面额得到
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
//这种写法最快,跳过了一些不必要的运算
class Solution {
public int coinChange(int[] coins, int amount) {
int[] am = new int[amount+1];
am[0] = 0;
for(int i = 1; i <= amount; i++){
int min = Integer.MAX_VALUE;
for(int coin : coins){
//跳过不存在的情况
if(i-coin >=0 && am[i-coin] >= 0){
min = Math.min(min, am[i-coin]+1);
}
}
am[i] = (min == Integer.MAX_VALUE) ? -1 : min;
}
return am[amount];
}
}