Python版-LeetCode 学习:322 零钱兑换问题

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

说明:你可以认为每种硬币的数量是无限的。
链接:https://leetcode-cn.com/problems/coin-change

解题思考:

硬币问题可以用数学表述成如下形式:

在不断的迭代dp()中,当出现n-coin==0时,兑换零钱加一次。

从递归的迭代过程思考,可以画出迭代树表示,例如假设amount=9,coins={1,2,5}

图中相同颜色的色块是在迭代过程中重复计算的部分。

方法1:递归

递归算法的时间复杂度分析:子问题总数 x 每个子问题的时间

子问题总数为递归树节点个数,这个比较难看出来,是 O(n^k),总之是指数级别的。每个子问题中含有一个 for 循环,复杂度为 O(k)。所以总时间复杂度为 O(k * n^k),指数级别

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        
        def count(n):
            # 基本情况的判定
            if n==0 : return 0
            if n<1 : return -1
            # 初始化
            min_coins=float('INF')
            for coin in coins:
                # 子问题的分解
                subproblem=count(n-coin)
                if subproblem == -1:
                    continue
                # 
                min_coins=min(min_coins,1+subproblem)
            # 如果一直continue,min_coins 不变,就代表没有可以分的可能
            return min_coins if min_coins !=float('INF') else -1

        return count(amount)

方法二: 加入记录表的迭代

「记录表」大大减小了子问题数目(即替代了上图中部分同色块的数值计算),完全消除了子问题的冗余,所以子问题总数不会超过金额数 n,即子问题数目为 O(n)。处理一个子问题的时间不变,仍是 O(k),所以总的时间复杂度是 O(kn)。

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        
        memo={}
        def count(n):
            # 查找子问题的值
            if n in memo:
                return memo[n]
            if n==0 : return 0
            if n<1 : return -1
            # 初始化
            min_coins=float('INF')
            for coin in coins:
                # 子问题分解
                subproblem=count(n-coin)
                if subproblem == -1:
                    continue
                min_coins=min(min_coins,1+subproblem)
            # 存储子问题的解
            memo[n]= min_coins if min_coins !=float('INF') else -1
            return memo[n]

        return count(amount)

方法三:用list代替dict中间表的迭代

采用“由底向上”的方式,从1,2,3。。。开始计算,直到n。

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        # 初始化结果集
        # 每一个下标对应对应amount,下标对应的最后元素值就是零钱的最小个数
        res=[float('INF')]*(amount+1)
        
        res[0]=0
        for i in range(amount+1):

            for coin in coins:
                # 不可分就下一轮
                if (i-coin) <0: continue
                # 对每一个i进行计算
                res[i]=min(res[i],1+res[i-coin])

        if res[amount] == float('INF'):
            return -1
        else:
            return res[amount]

方法四:利用整除减少循环次数

每次递归中判断剩余的amount%nums[index]是不是为0,如果是的话,表示当前的coins[index]可以将amount塞满,那么此时就是最优解了,记录下来;如果不是的话,那么我们只需要递归判断需要多少个coins[index]即可。如果遍历到最后一个coins[-1]且无法塞满amount的话,那么无解。

最后还有一个非常重要的剪枝,如果当前硬币coins[index](放满amount)放入后,硬币数比之前的结果都大的话,那么就不用继续判断后面的硬币了(因为硬币是排序过的)。

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        
        n = len(coins)
        coins.sort(reverse=True)
        self.res = float("inf")
        def dfs(index,target,count):
            coin = coins[index]
            if math.ceil(target/coin)+count>=self.res:
                return
            if target%coin==0:
                self.res = count+target//coin
            if index==n-1:return
            print([range(target//coin,-1,-1)])
            for j in range(target//coin,-1,-1):
                dfs(index+1,target-j*coin,count+j)
        dfs(0,amount,0)

        return int(self.res) if self.res!=float("inf") else -1

猜你喜欢

转载自blog.csdn.net/guyu1003/article/details/107182853
今日推荐