笔者第一想法是,得到所有数值和为amount的组合,取其中最短的组合长度返回,没有则返回-1。
根据这个思路,笔者做了深度优先遍历dfs。
初始因为剪枝不足而总是超时,最终代码为:
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
if amount==0:
return 0
coins.sort()
global ret
ret=float("inf")
def dfs(n,target,big):
#print(n,target,big)
global ret
#剪枝1,target比最小的面值还小,直接返回
if target<coins[0]:
return
#得到一种组合,更新ret
if target in coins:
ret=min(ret,n+1)
return
for index in range(big,-1,-1):
#剪枝2,最大次数*最大面值
if (ret-n)*coins[index]<target:
break
#剪枝3,本次面值大于target,因为coins排序后单调递减,进入下一面值即可
if coins[index]>target:
continue
else:
#直接dfs
dfs(n+1,target-coins[index],index)
dfs(0,amount,len(coins)-1)
if ret!=float("inf"):
return ret
else:
return -1
其中,最重要的剪枝在这里:
if (ret-n)*coins[index]<target:
break
即当当前最大可能的次数(ret-n),乘以最大的面值(coins[index],因为是从大到小的)也不能满足条件直接break。
加这句之前超时,加上之后超过97.6%的提交,没错,就是这么变态。
另外,在查看题解的时候学到了python中表示最大数值和最小数值的方式:
max_int=float("inf")
min_int=float("-inf")
这是官方题解中给出的两种动态规划的方法:
自顶向下和自底向上。
原文链接
状态转移方程是:dp[n]=dp[n-coin]+1
递归过程,从最大的amount开始,逐渐减coin。
递归出口:
(1)小于0表示上层递归时,硬币面值大于目标,返回-1;
(2)等于0表示上层递归时,硬币面值等于目标,返回0,上层递归时可进入if,最终传回1;
(3)如果for中减去所有硬币都没有修改mini值,就返回-1,表示本层也没有硬币组合可以组成目标。
这里非常非常非常有用的一点是
import functools
@functools.lru_cache(amount)
笔者理解是一种python装饰器,lru_cache的功能是暂存最大amount大小的结果,当再次调用dp函数时直接返回而不会再次进入函数,从而大大减少重复计算。
import functools
def coinChange(coins,amount):
@functools.lru_cache(amount)
def dp(rem):
if rem < 0: return -1
if rem == 0: return 0
mini = int(1e9)
for coin in coins:
res = dp(rem - coin)
if res >= 0 and res < mini:
mini = res + 1
return mini if mini < int(1e9) else -1
if amount < 1: return 0
return dp(amount)
非常清楚。
def coinChange(coins,amount):
dp=[float("inf")]*(amount+1)
dp[0]=0
for coin in coins:
for index in range(coin,amount+1):
dp[index]=min(dp[index],dp[index-coin]+1)
return dp[amount] if dp[amount]<float("inf") else -1