Leetcode学习计划之动态规划入门day20(322,518)

目录

322. 零钱兑换

问题描述

思路与算法1:动态规划

 思路与算法2:广度优先搜索

518. 零钱兑换 II

问题描述

思路与算法1:动态规划


322. 零钱兑换

问题描述

给你一个整数数组 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

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4

思路与算法1:动态规划

        记dp(x)表示用coins表示x所需要的最少硬币数。

        coins中面值比x更大的硬币肯定不能用。

  • 如果coins中恰好存在等于x的硬币,则结果显然为1。
  • 如果coins中没有小于等于x的硬币,则结果显然为-1。
  • 如果coins中有小于等于x的硬币,假设其集合为coins_small,则可以得到状态转移方程:

                dp(x) = \min\limits_{coin \in coins\_small}(1+dp(x-coin))

        为了方便以上实现,可以先对coins进行排序。前面两条构成了动态规划的基线条件。此外,如果amount等于0,则显然结果应该为0。

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if amount==0:
            return 0
        coins.sort(reverse=True)
        memo = dict()
        def dp(x):
            #print('dp({0})'.format(x))
            if x in memo:
                return memo[x]
            ans = -1
            for coin in coins:
                if coin < x:
                    tmp = dp(x-coin)
                    if tmp == -1:
                        pass
                    else:
                        ans = tmp+1 if ans == -1 else min(ans, tmp+1)
                elif coin == x:
                    ans = 1
                    break
                else:
                    pass
            memo[x] = ans
            return ans
        return dp(amount)

        执行用时:1384 ms, 在所有 Python3 提交中击败了34.79%的用户

        内存消耗:47 MB, 在所有 Python3 提交中击败了4.98%的用户

 思路与算法2:广度优先搜索

        本题还可以用广度优先搜索来解。

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        assert(amount >= 0)
        if amount == 0:
            return 0
        if amount < min(coins):
            return -1

        q = deque([amount])
        visited = set()
        layer = 1
        while q:            
            # print('layer = {0}, q.len = {1}'.format(layer,len(q)))
            n = len(q)
            for _ in range(n): # Traverse the current layer
                a = q.pop()
                if a in coins:
                    return layer
                # Add a's neighbours into q
                for c in coins:
                    b = a - c
                    if b > 0 and b not in visited:
                        visited.add(b)                        
                        q.appendleft(b)               
            layer = layer + 1
        return -1 

        执行用时:456 ms, 在所有 Python3 提交中击败了98.80%的用户

        内存消耗:15.9 MB, 在所有 Python3 提交中击败了17.40%的用户

        广度优先搜索的性能要比动态规划好很多。可能是因为动态规划算法采用的递归调用式的实现方式,如果改为迭代式的实现可能会好一些吧。

518. 零钱兑换 II

问题描述

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带符号整数。

例 1:

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例 2:

输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。

示例 3:

输入:amount = 10, coins = [10] 
输出:1

提示:

  • 1 <= coins.length <= 300
  • 1 <= coins[i] <= 5000
  • coins 中的所有值 互不相同
  • 0 <= amount <= 5000

思路与算法1:动态规划

        记dp(x)表示用coins表示x的所有组合数。

        对于coins中所有面值小于等于x的每个coin,x可以由表示x-coin的任一组合再加上一个coin而得。由于可得状态转移方程:

        dp(x) = \sum\limits_{coin \in coins \ and \ coin \leq x} dp(x-coin)

        基线情况:dp(0) = 1,因为不选择任何一种硬币也是一种组合

        但是这样(对dp[x]进行遍历,在内层对coins进行遍历)会导致有重复计算的情况。比如说,示例1的输入[(1,2,5), 5],5==>4会得到(2,2,1)的结果,5==>2也会得到(2,2,1)的结果,同样都会得到(1,1,1,1,1)的结果。

        排除组合的重复计数的关键在于遍历的顺序。如果改为外层循环对coins进行遍历,内层对dp[x]进行遍历则可以避免重复计数。

        由此可以得到本题的动态规划的做法:

  • 初始化 \textit{dp}[0]=1
  • 遍历 \textit{coins},对于其中的每个元素 \textit{coin},进行如下操作:
    • 遍历 i\textit{coin}\textit{amount},将 \textit{dp}[i - \textit{coin}] 的值加到 \textit{dp}[i]

        最终得到 \textit{dp}[\textit{amount}] 的值即为答案。

        上述做法不会重复计算不同的排列。因为外层循环是遍历数组 \textit{coins}的值,内层循环是遍历不同的金额之和,在计算 \textit{dp}[i] 的值时,可以确保金额之和等于 i 的硬币面额的顺序,由于顺序确定,因此不会重复计算不同的排列。相反,如果外层循环遍历\textit{dp}[i],内层循环coins,得到的其实是考虑不同顺序的排列数。     

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [0]*(amount + 1)
        dp[0] = 1
        for i in range(len(coins)):
            for j in range(coins[i], amount + 1):
                dp[j] += dp[j - coins[i]]
        return dp[amount]

        执行用时:128 ms, 在所有 Python3 提交中击败了73.51%的用户

        内存消耗:15.1 MB, 在所有 Python3 提交中击败了44.83%的用户

 

        回到总目录:Leetcode每日一题总目录(动态更新。。。)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笨牛慢耕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值