leetcode322 零钱兑换(贪心+回溯,暴力递归,备忘录递归,动态规划)

题目描述:给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。

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

题解1:回溯+贪心(用回溯的模板,贪心的思想)

  1. 卡的点是不知道怎么在递归中返回count的结果,解决方法就是不用返回,直接用一个全局变量存取count就行
  2. 递归中变量的命名一定要注意!因为在处在同一层递归时,for循环还会在这层递归中循环多次。下面的代码里一定要注意在for循环里要用new_count和new_amount,在递归中的变量命名太容易犯错了,要根据需求确保在递归每一层中的命名是否需要改变。如果用count和amount代替new_count和new_amount的话,在递归返回到上一层后,然后在走下一个for循环时,因为for循环的条件的变量,所以变量发生改变的话会导致for循环的条件发生改变。然后就是在递归遍历for循环的这一层时count作为一个变量应该也是不能改变的!
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        #采用贪心的思想,回溯的模板
        if amount == 0:
            return 0
        coins.sort(reverse=True)

        #声明了一个全局变量
        global min_count
        min_count = float("inf")
        self.backtracking(coins,amount,0,0)

        #考虑到没有面值能使得amount_last=0的情况
        ans = min_count if min_count != float("inf") else -1
        return ans

    def backtracking(self,coins,amount,idx,count):
        global min_count
        if idx >= len(coins):
            return
        #这个循环的设置就表示了每一步先选最大面值的硬币
        # 还表示了如果剩余的硬币面值大于总数也不行
        for i in range(amount//coins[idx],-1,-1):
            #如果用当前最大硬币的最大次数不行的话,在返回后,继续走当前for循环,for循环的amount变量在当前层应该不能被改变
            new_amount = amount - i*coins[idx]
            new_count = count + i
            if new_amount == 0:
                #因为每一步遍历都有一个新的count,所以不能直接将count赋值给min_count
                min_count = min(min_count,new_count)
                break
    
            #因为这里amount没有等于0,所以后面还至少都还需要一个硬币,
            #那么就直接可以不尝试了
            if count >= min_count -1:
                break
                
            self.backtracking(coins,new_amount,idx+1,new_count)

时间复杂度:O(n2)
空间复杂度:O(n)
refer:参考题解

题解2: 暴力递归
在这里插入图片描述

递归暴力求解,当我们面试过程中遇到这种问题时,如果一时没有思路,也要想到一种万能算法–暴力破解。这种方法为什么能找到最优值呢?因为暴力求解就是指遍历出所有的可能性,就是穷举所有的情况,然后每次都和最优值进行比较,最后取到最优值
大家发现了没有,我们的问题提可以不断被分解为「我们用现有的币值凑出 n 最少需要多少个币」,比如我们用 f(n) 函数代表 「凑出 n 最少需要多少个币]。

原问题:凑齐amount金额,最少需要多少枚硬币
子问题:凑齐amount−coin金额,最少需要多少枚硬币

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if amount == 0:
            return 0
        if amount<0:
            return -1
        res = float("inf")
        for i in coins:
            #subproblem为子问题,表示面值为amount时所需的最小硬币数
            subproblem = self.coinChange(coins,amount-i)
            if subproblem == -1:
                continue
            
            #???res和subproblem不会被上层覆盖
            #不会 因为每一层都把当前的res返回给上一层了 这是因为代码最后有个return
            #也就是说上一层保存的subproblem变量值就是这一层最后的res
            #然后返到上一层后,res又被马上更新成subproblem+1的值
            res = min(res,subproblem+1)
        if res == float("inf"):
            return -1
        else:
            return res
            

时间复杂度:对于一些比较正常的数据,这个代码都能正常运行,但是对于LeetCode中的[186,419,83,408] 6249,标准答案是20(最少用20个硬币),也就是递归至少有20层,因此计算量至少是3^20(每次节点数为3的n-1次方,然后知道层数后,将每层的节点数用等比数列公式求和就是总节点数),但是递归深度可以达到75层(全取最小面值83时),所以时间复杂度应该是3的75次方,计算量就是一个天文数字了。
refer
动态规划引入篇: 逐步探索找零问题
动态规划问题:零钱兑换的递归、备忘录、动态规划解法(Python)

题解3:在暴力递归的基础上加入备忘录(记忆化搜索),把存在重复的节点全部剔除,只保留一个节点即可,这样在遍历时,碰到直接见过的就直接拿来用的就行了,节省了时间复杂度。剔除完最后只会留下线性的节点形式,这个带备忘录的递归算法时间复杂度只有O(n),所以就是由暴力解的指数级的时间复杂度减少到线性时间复杂度了,已经跟动态规划的时间复杂度相差不大了。那么这不就可以了吗?为什么还要搞动态规划?因为递归仍然存在一个问题,就是爆栈!:
在这里插入图片描述
在这里插入图片描述

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        #声明了个全局变量,所有函数都能看到,这里如果把memo作为参数传进recursion函数也是可以的,因为是引用传递
        global memo
        memo = [-100 for i in range(amount+1)]
        return self.recursion(coins,amount)

    def recursion(self,coins,amount):
        if amount == 0:
            return 0
        
        if amount<0:
            return -1
        if memo[amount] != -100:
            return memo[amount]
        res = float("inf")
        for i in coins:
            #subproblem为子问题,表示面值为amount时所需的最小硬币数
            subproblem = self.recursion(coins,amount-i)
            if subproblem == -1:
                continue
            #???res和subproblem不会被上层覆盖
            res = min(res,subproblem+1)
        if res == float("inf"):
            memo[amount] = -1
        else:
            memo[amount] = res
        return memo[amount]

题解4:用动态规划算法,虽然带备忘录的递归算法时间复杂度只有O(n),但是由于递归深度的原因需要 O(n) 的空间复杂度,容易爆栈(每一个函数调用,都要在栈区创建一个空间,能用动态规划或者迭代,就不用递归,因为递归太耗堆栈了,效率不高)。而换成迭代之后我们根本不需要如此多的的储存空间,这个技巧就是所谓的「状态压缩」,来缩小 DP table 的大小,只记录必要的数据,空间复杂度缩小到常数级别。但我们这里的题解并没有用这个方法,而是用一个 DP table 存储了所有的状态。
过大数组导致爆栈的解决方法记录(堆栈)

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        #初始化备忘录,用amount+1填满备忘录,amount+1 表示该值不可以用硬币凑出来
        dp = [amount+1 for _ in range(amount+1)]
        #设置初始条件为 0
        dp[0] = 0
        #去遍历这个数组
        for i in range(len(dp)):
            for coin in coins:
                #根据动态转移方程求出最小值
                if i-coin<0:
                    continue
                dp[i] = min(dp[i],1+dp[i-coin])
        #如果 `dp[amount] === amount+1`说明没有最优解返回-1,否则返回最优解
        if dp[amount] == amount+1:
            return -1
        else:
            return dp[amount]
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值