leetcode 322.零钱兑换 (python)

在这里插入图片描述
题解:python

这是动态规划问题,动态规划问题通常用来求最值;web

动态规划三要素:算法

状态:当前的金额数量
状态转移方程:当前金额是n,至少须要dp[n]个硬币凑出当前金额;
最优子结构:若是当前金额n=0,则返回0;若是当前金额n<0, 则返回-1;svg

暴力递归法:(自顶向下)
递归终止条件:ui

  1. 若是当前金额n=0,则返回0;若是当前金额n<0, 则返回-1;
    在这里插入图片描述
    详述递归过程,以coins=[1,2,5],金额11为例;
    n = 11, n-1=10;
    n = 10, n - 1= 9;

    而后一直递归直到
    n = 1, n - 1 = 0;
    返回0后,res= 1;
    而后
    n = 1, n- 2= -1
    ------, n -5= -4
    即此时 n = 1的for循环结束,返回res = 1;此时计算出了dp[1]的最优解;

    此时n = 2, n - 1= 1时递归返回1;
    此时res = 2;
    而后
    n = 2, n - 2 = 0;返回0,res = min(2, 1)= 1;
    n = 2, n -5 = -3; 返回-1
    此时循环结束,返回res=1;此时计算出了dp[2]的最优解;
    返回上一层n = 3.按照此方法进行回溯。

代码以下:spa

class Solution:
    def coinChange(self, coins, amount):
        def dfs(n):

            ##递归终止条件

            if n == 0:
                return 0
            if n < 0:
                return -1

            res = float("inf")

            for i in range(len(coins)):

                sub_problem = dfs(n - coins[i])

                #子问题无解,跳过
                if sub_problem == -1:
                    continue

                #res记录子问题的最优解
                res = min(res, 1 + sub_problem)
                
            if res == float("inf"):
                return -1
            else:
                return res

        return dfs(amount)

可是上述方法会引入冗余计算;
以下图所示:
在这里插入图片描述
备忘录算法:
创建一个字典记录已经计算过的金额,避免重复计算;code

class Solution:
    def coinChange(self, coins, amount):

        memo = dict()


        #备忘录算法
        def dfs(n):

            if n in memo:return memo[n]

            if n == 0:
                return 0
            if n < 0:
                return -1


            res = float("inf")


            for coin in coins:

                sub_problem = dfs(n - coin)

                #子问题无解,跳过
                if sub_problem == -1:

                    continue

                #res记录子问题的最优解
                res = min(res, 1 + sub_problem)
                


            if res == float("inf"):
                return -1
            else:
                memo[n] = res
                return res

        return dfs(amount)

自底向上的算法:
问题分解:
参考自leetcode的官方题解:
在这里插入图片描述
我的理解:
对于当前的金额i,须要遍历每个硬币,来找i-coins的最小值;以总金额为2,coins=[1,2]为例;
F(2) = min(F(2-1),F(2-2)) + 1 = 1
代码以下,两层循环实现;第一层循环对金额进行遍历,求出每个金额的最优硬币数量;
第二层,遍历每个硬币,找到当前金额时须要的最小硬币数;但在进行循环以前,须要判断当前金额和硬币的大小关系,不然会出现金额小于硬币的状况,产生负数;xml

class Solution:
    def coinChange(self, coins, amount):

    	dp = [amount + 1]*(amount + 1)

    	dp[0] = 0


    	for i in range(1,amount + 1):
    		for j in range(len(coins)):

    			if i >= coins[j]:

    				dp[i] = min(dp[i], dp[i - coins[j]] + 1)

    	if dp[amount] == amount + 1:
    		return -1
    	else:
    		return dp[amount]

这个代码和上一个代码思想一致,可是循环倒是相反的;
下面这个代码是先求出在当前的一个硬币下,dp[i]的值,此时在一个硬币循环完成后,获得的dp[i]并非最优的,而是在遍历每个硬币的过程当中,求出dp[i]的最优值;
例如金额为2,coins为[1,2]时;
硬币为1时,dp[1] = min(dp[1],dp[0] + 1) = 1, dp[2] = min(dp[2],dp[2-1] + 1) = 2;
硬币为2时,___________________________dp[2] = min(dp[2],dp[2-2] + 1) = 1
同时,下述代码在第二层循环中不用判断金额和当前硬币的大小(由于其是从当前硬币的金额大小开始进行循环的);而在第一个代码中须要判断,由于金额是从1开始循环;所以,第二个的时间复杂度是小于第一个的,效率更高。blog

class Solution:
    def coinChange(self, coins, amount):

    	dp = [amount + 1]*(amount + 1)

    	dp[0] = 0

    	for i in range(len(coins)):
    		for j in range(coins[i], amount + 1):#避免了用if条件语句



    			dp[j] = min(dp[j], dp[j - coins[i]] + 1)

    	if dp[amount] == amount + 1:
    		return -1
    	else:
    		return dp[amount]