【JS每日一算法:剑指Offer系列】🟨198.零钱兑换(深度优先、深度优先+剪枝、动态规划)

62 阅读3分钟

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例 1:

输入: coins = [1, 2, 5], amount = 11
输出: 3 
解释: 11 = 5 + 5 + 1

示例 2:

输入: coins = [2], amount = 3
输出: -1

示例 3:

输入: coins = [1], amount = 0
输出: 0

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 231 - 1
  • 0 <= amount <= 104

题解:

个人博客

更多JS版本题解点击链接关注该仓库👀

/**
 * @description: 深度优先  TC:O(n^3)  SC:O(n)
 * @param {*} coins  给定整数数组
 * @param {*} amount 给定整数
 */
function dfs(coins, amount) {
    /**
     * 本方案使用深度优先,利用递归不断对amount
     * 减去所有硬币面额,直到amount小于等于0结束
     * 递归,等于0证明硬币能够组成总金额,然后记
     * 录比较最小硬币数即可,小于0证明硬币无法组
     * 成总金额无需处理
     */

    // 记录组成总金额的最小硬币数
    let minCount = Infinity
    /**
     * @description: 递归实现深度优先
     * @param {*} diff 当前剩余总金额
     * @param {*} count 已组合的硬币数量
     */
    function recursion(diff, count) {
        // 如果当前剩余总金额等于0,证明硬币能够
        // 组成总金额,然后记录比较最小硬币数,
        // 结束递归
        if (diff == 0) {
            minCount = Math.min(minCount, count)
            return
        }
        // 如果当前剩余总金额小于0证明硬币无法组
        // 成总金额无需处理结束递归
        if (diff < 0) return
        // 否则,不断对amount减去所有硬币面额,继续递归
        for (let coin of coins) dfs(diff - coin, count + 1)
    }
    // 执行递归
    recursion(amount, 0)
    // 返回结果
    return minCount == Infinity ? -1 : minCount;
}

/**
 * @description: 深度优先+剪枝  TC:O(n^2)  SC:O(n)
 * @param {*} coins  给定整数数组
 * @param {*} amount 给定整数
 */
function DFSAndPruningWay(coins, amount) {
    /**
     * 上述方案是会超时的,这是因为递归过程中会进行大
     * 量的重复计算,我们可以利用map记录下已经计算过
     * 的金额的最小组成硬币数,后续递归就无需再次计算
     * 以此优化时间复杂度
     */

    // 利用map记录已经计算过的金额的最小组成硬币数
    let cache = new Map()

    /**
     * @description: 递归实现深度优先
     * @param {*} diff 当前剩余总金额
     */    
    function recurison(diff) {
        // 如果当前剩余总金额等于0,证明硬币能够
        // 组成总金额,结束递归返回0
        if (diff == 0) return 0;
        // 如果当前剩余总金额小于0证明硬币无法组
        // 成总金额,结束递归返回-1
        if (diff < 0) return -1;
        // 如果当前剩余总金额已经计算过,
        // 则返回已经计算过的金额的最小
        // 组成硬币数
        if (cache.has(diff)) return cache.get(diff);

        let minCount = Infinity;
        
        // 否则,不断对amount减去所有硬币面额,继续递归
        for (let coin of coins) {
            subCount = recurison(diff - coin)
            // 如果返回不等于-1,证明硬币能够
            // 组成总金额,记录比较最小硬币数
            if (subCount != -1) minCount = Math.min(minCount, subCount + 1)
            // 否则则无法组成总金额,无需处理
        }
        // 如果minCount还是等于Infinity,证明
        // 无法组成总金额,将minCount置为-1
        if (minCount == Infinity) minCount = -1
        // 存储当前总金额的计算结果
        cache.set(diff, minCount)
        // 返回结果
        return minCount
    }
    // 执行递归,返回结果
    return recurison(amount);
}

/**
 * @description: 动态规划  TC:O(n^2)  SC:O(n)
 * @param {*} coins  给定整数数组
 * @param {*} amount 给定整数
 */
function dp(coins, amount) {
    /**
     * 本方案使用DP,DP通常就是剪枝的回溯过程,DFS是
     * 自顶向下,然DP则是自底向上,因此如果理解了上述
     * 两种方案,DP自然是很好理解的
     */
    let DPArray = new Array(amount + 1).fill(amount + 1)
    DPArray[0] = 0
    for (let i = 1; i < DPArray.length; i++) {
        for (let coin of coins) {
            if (i - coin < 0) continue;
            DPArray[i] = Math.min(DPArray[i], 1 + DPArray[i - coin])
        }
    }
    return DPArray[amount] == amount + 1 ? -1 : DPArray[amount]
};