[LeetCode] 322.零钱兑换 五种方法讲解

322.零钱兑换 五种方法讲解

文章目录

  • 322.零钱兑换 五种方法讲解
    • 1 问题描述
    • 2 问题分析
    • 3 解决策略
      • 3.1 递归-暴力解决
    • 3.2 递归-加入存储
    • 3.3 BFS
    • 3.4 动态规划-自上而下
    • 3.5 动态规划-自下而上

1 问题描述

给你一个整数数组 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

2 问题分析

给人的第一直接就是让程序去尝试所有的可能性,然后将其中所需硬币数量最少的结果进行返回。当需要尝试所有可能性的时候,我们往往会用到树(Tree)这种数据结构。我们假设以coins=[1,2,5], amount=11为例子,试图去用树对这个问题进行建模。我们假定每个节点的数字代表的是当前的金额。
[LeetCode] 322.零钱兑换 五种方法讲解_第1张图片
可以看到,从根节点11出发,我们有三个硬币,所以有三个分支,而每一个金额又可以对应新的三个节点,如果节点的值为0,也就是说当前的金额就是0,说明任务完成了,我们成功凑出了金额,如果小于0,那就说明当前分支不能够凑出该金额。

3 解决策略

3.1 递归-暴力解决

根据上面的分析,发现这个问题很容易可以被拆解成递归的方式去解决,每个子问题就是找到当前金额所需的最少硬币数量,如果当前金额为0,那么就返回当前硬币的数量,这是递归的退出条件之一,除此之外,当金额小于0时,也需要返回值。考虑到所需要的硬币数量不可能大于amount + 1,在金额小于0时,说明不可以凑成我们想要的数,那么我们就返回一个amount + 1来表示当前的分支是不能凑成所需金额的。

class Solution(object):
	def coinChange(self, coins, amount):
    	def helper(coins, curAmount, numOfCoins):
            globalMin = amount + 1
            if curAmount == 0:
                return numOfCoins
            if curAmount < 0:
                return amount + 1
            for coin in coins:
                curMin = helper(coins, curAmount - coin, numOfCoins + 1)
                globalMin = min(curMin, globalMin)
            return globalMin
       	return helper(coins, amount, 0)

时间复杂度: O ( n S ) O(n^S) O(nS), 其中 S 是要凑的金额数amount,而 n 是所拥有的硬币数量。这是因为考虑到树结构最多的层数不会超过 S S S,而分支的数量则取决于硬币的个数。
空间复杂度: O ( S ) O(S) O(S),因为至多需要存储 S S S个值来得到amount

3.2 递归-加入存储

如果细心的话会发现,在计算过程中有很多重复计算的部分,所以我们可以加入存储,如果当前的金额已经被计算出来,那我们就直接返回结果即可。

class Solution(object):
    def coinChange(self, coins, amount):
        self.cache = {}
        def helper(coins, curAmount):
            if curAmount in self.cache:
                return self.cache[curAmount]
            globalMin = amount + 1
            if curAmount == 0:
                return 0
            if curAmount < 0:
                return amount + 1
            for coin in coins:
                curMin = helper(coins, curAmount - coin)
                if curMin == globalMin:
                    continue
                globalMin = min(curMin + 1, globalMin)
            self.cache[curAmount] = globalMin
            return globalMin
        
        result = helper(coins, amount)
        return -1 if result == amount + 1 else result

这样做之后时间复杂度就可以减少到 O ( S n ) O(Sn) O(Sn),这是因为每一次的计算步骤至多需要 S S S次运算,而我们每次都需要计算所有不同面额的硬币的可能性,所以一共需要计算两者的乘积。

3.3 BFS

上述两种递归方法更偏向于深度优先遍历,但是就我们的问题而言,其实并不需要这样做,实际上,如果使用BFS遍历的方法,当我们找到第一次amount为0的时候,就已经代表我们找到了结果,因为当前树的层数一定是最少的。

import collections
class Solution(object):
    def coinChange(self, coins, amount):
        q = collections.deque([(amount, 0)])
        seen = set([amount])
        
        while q:
            curAmount, numOfCoins = q.popleft()
            if curAmount == 0:
                return numOfCoins
            for coin in coins:
                if curAmount - coin >= 0 and curAmount - coin not in seen:
                    q.append((curAmount - coin, numOfCoins + 1))
                    seen.add(curAmount - coin)
        return -1

3.4 动态规划-自上而下

如果从动态规划的角度来考虑问题,还是以老例子为例,想要求解 F ( 11 ) F(11) F(11),那其实就是求解 m i n ( F ( 11 − c o i n 1 ) + 1 , F ( 11 − c o i n 2 ) + 1 , F ( 11 − c o i n 3 ) + 1 ) min(F(11-coin_1)+1, F(11-coin_2)+1,F(11-coin_3)+1) min(F(11coin1)+1,F(11coin2)+1,F(11coin3)+1),而当中的每一项都可以被继续分解,直到 F ( 0 ) F(0) F(0)。而 F ( 0 ) F(0) F(0)的意思是凑出0元所需的硬币个数,结果显然是0。

class Solution(object):
    def coinChange(self, coins, amount):
        seen = {}
        def helper(curAmount):
            if curAmount in seen:
                return seen[curAmount]
            if curAmount == 0:
                return 0
            if curAmount < 0:
                return amount + 1
            minNumOfCoins = amount + 1
            for coin in coins:
                minNumOfCoins = min(helper(curAmount - coin) + 1, minNumOfCoins)
            seen[curAmount] = minNumOfCoins
            return minNumOfCoins
        result = helper(amount)
        return result if result != amount + 1 else -1

3.5 动态规划-自下而上

有了上面的思路,我们同样可以从反方向来考虑这个问题,先给定 F ( 0 ) F(0) F(0),然后再去求解 F ( 1 ) F(1) F(1) F ( 2 ) F(2) F(2),以此类推,直到 F ( a m o u n t ) F(amount) F(amount)为止。

import collections
class Solution(object):
    def coinChange(self, coins, amount):
        q = collections.deque([(amount, 0)])
        seen = set([amount])
        
        while q:
            curAmount, numOfCoins = q.popleft()
            if curAmount == 0:
                return numOfCoins
            for coin in coins:
                if curAmount - coin >= 0 and curAmount - coin not in seen:
                    q.append((curAmount - coin, numOfCoins + 1))
                    seen.add(curAmount - coin)
        return -1

你可能感兴趣的:(LeetCode,leetcode,算法,职场和发展,python)