322. Coin Change

本文探讨了硬币找零问题的动态规划(DP)解决方案,对比了自顶向下与自底向上的DP方法,并提供了LeetCode的两种解决方案。自顶向下的方法采用递归加备忘录,而自底向上则使用迭代和表格记录子问题答案。

题目

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];
    }
    
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值