leetcode 322 零钱兑换


1. 题目

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

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

示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。

2. 解法

2.1. 暴力穷举

暴力穷举实质就是一个个方案试,如果满足条件则记录其使用的硬币数量,再从中取最小值。

2.1.1. 暴力穷举1

注意:并不需要穷尽所有组合,从大数开始组合,求出一个有效组合的硬币数作为最小值过滤器,可以减少无效递归次数;

    def coinChange_1(self, coins, amount):
        """
        暴力穷举
        递归
        def _helper(coins, num, leftamount, cur_coins_list):
        递归函数,coins:硬币池; num:下标; leftamount:剩余钱数; cur_coins_list:当前硬币序列
        时间复杂度:O(S^N);因为最坏情况下每种硬币最多可能有S/Ci个;
        所以可能的组合数为S/C1 * S/C2 * S/C3 ......S/Cn=S^N/C1*C2*C3......Cn
        简化一下就是S^N
        S为金额,N为硬币种数。
        空间复杂度:O(N)最坏情况下,递归的最大深度为N。
        :param coins:
        :param amount:
        :return:
        """
        assert coins is not None and amount > 0, 'value error.'
        mincount = amount + 1
        # 反转coins排序方式为大至小
        coins = list(reversed(coins))

        # 递归函数
        def _helper(coins, num, leftamount, cur_coins_list):
            nonlocal mincount
            cur_coins_list.append(coins[num])

            length = len(cur_coins_list)
            if leftamount == 0:
                if length < mincount:
                    mincount = length
            elif leftamount > 0:
                if length >= mincount:
                    return
                for i in range(num, len(coins)):
                    _helper(coins, i, leftamount-coins[i], cur_coins_list[:])

        for x in range(len(coins)):
            _helper(coins, x, amount - coins[x], [])

        return -1 if mincount > amount else mincount

2.1.2. 暴力穷举2

同样的逻辑,换个方式。

    def coinChange_1_1(self, coins, amount):
        """
        暴力穷举
        :param coins:
        :param amount:
        :return:
        """
        coins = list(reversed(coins))
        def _helper(index, coins, amount):
            if amount == 0: return 0

            if index < len(coins) and amount > 0:
                mincost = float('inf')
                for i in range(0, amount//coins[index]+1):
                    if amount >= i * coins[index]:
                        res = _helper(index+1, coins, amount - i * coins[index])
                        if res != -1:
                            mincost = min(mincost, res+i)
                return -1 if mincost == float('inf') else mincost
            return -1
        return _helper(0, coins, amount)

2.2. 动态规划-1 自下而上

定义一维数组dp
dp[i]的值为组合成i时需要的最少硬币数,那么继续向前推就是dp[i]=dp[i-coin[j]] 需要的最少硬币数 +1,+1代表使用coin[j]一次。

原理:

定义数组dp[i][j],i为硬币数量,j为总金额,该处元素定义为如果j可以由coins[0-i]中的硬币组成,则值为组成方案所需硬币数量的最小值。

设dp[x][y]存在解,那么dp[x][y-coin[x]]一定有解(除非y小于x),  
而且dp[x][y]的可能值为dp[x][y-coin[x]] + 1 (在前者的基础上多加了一个coin[x])  
但是,注意列j代表此金额下的所有解,所以如果dp[x-1][y]存在解,意味着该解对dp[x][y]也是有效的。  
题目要求的是解的最小值硬币数,所以二者中取最小值:  
dp[x][y] = min(dp[x][y-coin[x]], dp[x-1][y])

当然,这是一个二维数组,但实际中并不关心[x-2]及以前行的数据,而且是逐一向后迭代的,所以可以复用简化为一维数组。

代码:

    def coinChange_2(self, coins, amount):
        """
        动态规划
        从下至上
        :param coins:
        :param amount:
        :return:
        """
        dp = [float('inf')] * (amount + 1)
        dp[0] = 0

        for coin in coins:
            for x in range(coin, amount + 1):
                dp[x] = min(dp[x], dp[x-coin] + 1)
        return dp[amount] if dp[amount] != float('inf') else -1

2.3. 动态规划-2 自上而下

递归法的变种,使用数组记录已求过的解。

    def coinChange_2_2(self, coins, amount):
        """
        动态规划/递归穷举/回溯法
        自上而下
        :param coins:
        :param amount:
        :return:
        """
        def _helper(coins, leftamount, count_list):
            if leftamount < 0: return -1
            if leftamount == 0: return 0
            if count_list[leftamount-1] != 0: return count_list[leftamount - 1]
            mincount = float('inf')
            for x in range(len(coins)):
                res = _helper(coins, leftamount - coins[x], count_list)
                if res >= 0 and res < mincount:
                    mincount = res + 1
            count_list[leftamount - 1] = mincount if mincount != float('inf') else -1
            return count_list[leftamount - 1]

        return _helper(coins, amount, [0]*amount)

3. 代码及测试结果

3.1. 测试代码:

if __name__ == "__main__":
    # 实例化解决方案类
    so = Solution()

    # 参数设定
    li = [1,2,5,10,20]
    para = (li, 118)

    test_func(so, para)
    pass


3.2. 结果:

 共计有<4>个方法: ['coinChange_1_1', 'coinChange_1_2', 'coinChange_2_1', 'coinChange_2_2']

 **************************************** 
方法[1]:coinChange_1_1
说明:暴力穷举
        递归
        def _helper(coins, num, leftamount, cur_coins_list):
        递归函数,coins:硬币池; num:下标; leftamount:剩余钱数; cur_coins_list:当前硬币序列
        时间复杂度:O(S^N);因为最坏情况下每种硬币最多可能有S/Ci个;
        所以可能的组合数为S/C1 * S/C2 * S/C3 ......S/Cn=S^N/C1*C2*C3......Cn
        简化一下就是S^N
        S为金额,N为硬币种数。
        空间复杂度:O(N)最坏情况下,递归的最大深度为N。
        :param coins:
        :param amount:
        :return:
执行结果: 9

 **************************************** 
方法[2]:coinChange_1_2
说明:暴力穷举
        :param coins:
        :param amount:
        :return:
执行结果: 9

 **************************************** 
方法[3]:coinChange_2_1
说明:动态规划
        从下至上
        :param coins:
        :param amount:
        :return:
执行结果: 9

 **************************************** 
方法[4]:coinChange_2_2
说明:动态规划/递归穷举/回溯法
        自上而下
        :param coins:
        :param amount:
        :return:
执行结果: 9

执行时长:
('coinChange_1_1', 0.0018791932531923659)
('coinChange_1_2', 0.10514478138982856)
('coinChange_2_1', 0.00021833724987438408)
('coinChange_2_2', 0.0006242205989998584)

Process finished with exit code 0


猜你喜欢

转载自www.cnblogs.com/wodeboke-y/p/12391282.html
今日推荐