给你一个整数数组 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
给人的第一直接就是让程序去尝试所有的可能性,然后将其中所需硬币数量最少的结果进行返回。当需要尝试所有可能性的时候,我们往往会用到树(Tree)这种数据结构。我们假设以coins=[1,2,5], amount=11
为例子,试图去用树对这个问题进行建模。我们假定每个节点的数字代表的是当前的金额。
可以看到,从根节点11出发,我们有三个硬币,所以有三个分支,而每一个金额又可以对应新的三个节点,如果节点的值为0,也就是说当前的金额就是0,说明任务完成了,我们成功凑出了金额,如果小于0,那就说明当前分支不能够凑出该金额。
根据上面的分析,发现这个问题很容易可以被拆解成递归的方式去解决,每个子问题就是找到当前金额所需的最少硬币数量,如果当前金额为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
。
如果细心的话会发现,在计算过程中有很多重复计算的部分,所以我们可以加入存储,如果当前的金额已经被计算出来,那我们就直接返回结果即可。
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次运算,而我们每次都需要计算所有不同面额的硬币的可能性,所以一共需要计算两者的乘积。
上述两种递归方法更偏向于深度优先遍历,但是就我们的问题而言,其实并不需要这样做,实际上,如果使用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
如果从动态规划的角度来考虑问题,还是以老例子为例,想要求解 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(11−coin1)+1,F(11−coin2)+1,F(11−coin3)+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
有了上面的思路,我们同样可以从反方向来考虑这个问题,先给定 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