算法53----换钱的最小次数和方法数

Posted lee-yl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法53----换钱的最小次数和方法数相关的知识,希望对你有一定的参考价值。

 


一、题目:换钱的最小次数

  给定数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求组成aim的最少货币数。


  举个例子
  arr[5,2,3] ,aim=20
   4张5元可以组成20,并且是最小的,所以返回4
  arr[5,2,3],aim=0。
    不用任何货币就可以组成0元,这里返回0.
  arr[5,2,3],ami=4
    这里无法组成返回-1

思路:时间和空间都为O(M*aim)【len(arr) = M 】

dp 矩阵为:大小为M*aim

dp[i][j]的含义是在可以任意使用arr[0…i]货币的情况下,组成j所需的最小张数,矩阵的每一行和每一列可以先确定,其他的位置dp[i][j] = min( dp[i-1][j] , dp[i][j-arr[i]]+1)。

dp的第一行表示:只用 5 来分别构成 0……20 最少要多少张,比如 5 需要 1张,10需要2张,……构不成的赋系统最大值,最后返回-1。

dp的第二行表示:只用5和2分别构成0到20 最少要多少张。

dp的第三行表示:只用5,2,3分别构成0到20最少要多少张。

技术分享图片

 

dp矩阵的构建:初始化系统最大值

  • 第一列都为0,因为0不需要任何币来构成。
  • 第一行:能被5整除的赋值,值为整除的数。比如:5赋值1,10赋值2,15赋值3,20赋值4。
  • 第二行、第三行……:
  1. 比如:dp 第2行第7列的值 2 是根据
  2. dp [7-2] +1 = 1 +1 =2来的,即【7可以由1个5和1个2组成】,
  3. 但是还要看第一行7列的值是不是更小,即 dp [i][j] = min( dp [j-arr[i]] +1dp [i-1][j] ),dp [j-arr[i]] +1不能是系统最大值。

 代码1:

import sys
def minCoins1(arr, aim):
    if arr == None or len(arr) == 0 or aim < 0:
        return -1
    row = len(arr)
    dp = [[sys.maxsize for i in range(aim+1)] for j in range(row)]
    for i in range(row):
        dp[i][0] = 0
    for j in range(1, aim+1):
        if j % arr[0] == 0:
            dp[0][j] = j // arr[0]
    for i in range(1, row):
        for j in range(1, aim+1):
            left = sys.maxsize
            if j - arr[i] >= 0 and dp[i][j-arr[i]] != sys.maxsize:
                left = dp[i][j-arr[i]] + 1
            dp[i][j] = min(left, dp[i-1][j])
    return dp[row-1][aim] if dp[row-1][aim] != sys.maxsize else -1
arr = [5,2,3]
aim = 20
minCoins1(arr, aim)

 代码2:时间为O(M*aim)【len(arr) = M 】,空间为O(aim+1),dp 大小为 aim+1的列表

#经过空间压缩的动态规划
def minCoins2(arr, aim):
    if arr == None or len(arr) == 0 or aim < 0:
        return -1
    row = len(arr)
    dp = [sys.maxsize for i in range(aim+1)]
    dp[0] = 0
    for i in range(1, aim+1):
        if i % arr[0] == 0:
            dp[i] = i // arr[0]
    for i in range(1, row):
        dp[0] = 0
        for j in range(1, aim+1):
            left = sys.maxsize
            if j - arr[i] >= 0 and dp[j-arr[i]] != sys.maxsize:
                left = dp[j-arr[i]] + 1
            dp[j] = min(left, dp[j])
    return dp[aim] if dp[aim] != sys.maxsize else -1

 


题目二:换钱的方法数

https://blog.csdn.net/qq_34342154/article/details/77122125

给定数组arr,arr中所有的值都为整数且不重复。每个值代表一种面值的货币,每种货币有无数张,再给定一个整数aim代表要找的钱数,求换钱的方法有多少种。

思路1:暴力递归,最坏情况下时间O(aim^N),N表示数组的长度

首先介绍暴力递归的方法。如果arr = [5, 10, 25, 1],aim = 1000,分析过程如下:

用0张5元的货币,让[10, 25, 1]组成剩下的1000,最终方法数记为res1。
用1张5元的货币,让[10, 25, 1]组成剩下的995,最终方法数记为res2。
用2张5元的货币,让[10, 25, 1]组成剩下的990,最终方法数记为res3。
……
用201张5元的货币,让[10, 25, 1]组成剩下的0,最终方法数记为res201。

那么res1 + res2 + res3 + …… +res201的值就是中的方法数。
技术分享图片

 


#暴力递归方法
def coins1(arr, aim):
    def process1(arr, index, aim):
        if index == len(arr):
            return 1 if aim == 0 else 0
        else:
            res = 0
            for i in range(0, aim//arr[index]+1):
                res += process1(arr, index+1, aim-arr[index]*i)
        return res


    if arr == None or len(arr) == 0 or aim < 0:
        return 0
    return process1(arr, 0, aim)

思路2:记忆搜索:时间复杂度为O(N*aim^2),空间复杂度O(N*aim)。

采用一个字典存储计算过的结果,暴力递归会重复计算结果。比如使用0张5元+1张10元的情况和使用2张5元+0张10元的情况,都需要求[25, 1]组成剩下的990的方法数。记忆搜索就是使用一张记录表将递归过程中的结果进行记录,当下次再遇到同样的递归过程,就直接使用表中的数据。

 技术分享图片

第三行:圈出的

第一步:当0个5:当0个2,只用【3】构成15的方法:1个

         1个2,只用【3】构成13的方法:-1个【不可能】

         2个2,……

         ……

         7个2……

技术分享图片

第三行:圈出的

第二步:当1个5时,当0个2,只用【3】构成10:-1个

             1个2,只用【3】构成8:-1个

          ……

          5个2,只用【3】构成0:1个

技术分享图片

第二行:

当0个5时:只用【2,3】构成15有3种方法

当1个5时:只用【2,3】构成10有2种方法

当2个5时:只用【2,3】构成5有1种方法

当3个5时:只用【2,3】构成0有1种方法

技术分享图片

 

第一行:结果,用【5,2,3】构成15有7种方法

 代码:

def coins2(arr, aim):
    def process2(arr, index, aim, records):
        if index == len(arr):
            return 1 if aim == 0 else 0
        else:
            res = 0
            for i in range(0, aim//arr[index]+1):
                mapValue = records[index+1][aim-arr[index]*i]
                if mapValue != 0:
                    res += mapValue if mapValue != -1 else 0
                else:
                    res += process2(arr, index+1, aim-arr[index]*i, records)
        records[index][aim] = -1 if res == 0 else res
        return res


    if arr == None or len(arr) == 0 or aim < 0:
        return 0
    records = [[0 for i in range(aim+1)] for j in range(len(arr)+1)]
    return process2(arr, 0, aim, records)
arr = [5,2,3]
aim = 15
res1 = coins2(arr,aim)

 思路3:动态规划:时间O(M*aim^2),空间O(M*aim)

技术分享图片

 技术分享图片

为何绿圈等于上一行红圈的加和?即num += dp[i-1][j-arr[i]*k]

比如绿圈的第二行第5列那个1:只用【5,2】

上一行红圈表示只用【5】构成0,2,4的方法数为1,0,0

则当用0个2时,对应【5】构成4方法数为0

当用1个2时,对应【5】构成2方法数为0

当2个2时,对应【5】构成0方法数为1

代码:

def coins3(arr, aim):
    if arr == None or len(arr) == 0 or aim < 0:
        return 0
    row = len(arr)
    dp = [[0 for i in range(aim+1)]for j in range(row)]
    for i in range(row):
        dp[i][0] = 1
    for j in range(1, aim//arr[0]+1):
        dp[0][arr[0]*j] = 1
    for i in range(1, row):
        for j in range(1, aim+1):
            num = 0
            for k in range(j//arr[i]+1):
                num += dp[i-1][j-arr[i]*k]
            dp[i][j] = num
    return dp[row-1][aim]

思路4:动态规划时间的优化:时间O(M*aim),空间O(M*aim)

思路3中的第三步循环k可省略。

for k in range(j//arr[i]+1):
          num += dp[i-1][j-arr[i]*k] 即

dp[i][j] = dp[i-1][j] + dp[i-1][j-arr[i]] + dp[i-1][j-2*arr[i]] + …dp[i-1][j-k*arr[i]]

等价于

dp[i][j] = dp[i-1][j] + dp[i][j-arr[i]]

技术分享图片

代码:

def coins4(arr, aim):
    if arr == None or len(arr) == 0 or aim < 0:
        return 0
    row = len(arr)
    dp = [[0 for i in range(aim+1)] for j in range(row)]
    for i in range(row):
        dp[i][0] = 1
    for j in range(1, aim//arr[0]+1):
        dp[0][arr[0]*j] = 1
    for i in range(1,row):
        for j in range(1, aim+1):
            dp[i][j] = dp[i-1][j]
            dp[i][j] += dp[i][j-arr[i]] if j-arr[i] >= 0 else 0
    return dp[row-1][aim]

思路5:动态规划空间的优化:时间O(M*aim),空间O(aim)。

def coin5(arr,aim):
    if not arr or not aim:
        return 0
    dp = [0] * (aim + 1)
    for i in range(0,aim+1,arr[0]):
        dp[i] = 1
    for i in range(1,len(arr)):
        for j in range(1,aim+1):
            dp[j] += dp[j-arr[i]] if j - arr[i] >= 0 else 0
    return dp[-1]
arr = [5,2,3]
aim = 15
coin5(arr,aim)

 


















以上是关于算法53----换钱的最小次数和方法数的主要内容,如果未能解决你的问题,请参考以下文章

换钱的最少货币数(NC126/考察次数Top69/难度简单)

换钱的最少货币数(NC126/考察次数Top69/难度简单)

换钱的最少货币数(NC126/考察次数Top69/难度简单)

算法之Python实现 - 002 : 换钱的最少货币数补充(每种货币只能使用一次)

NC41 最长无重复子数组/NC133链表的奇偶重排/NC116把数字翻译成字符串/NC135 股票交易的最大收益/NC126换钱的最少货币数/NC45实现二叉树先序,中序和后序遍历(递归)(代码片段

动态规划专题3:换钱的最少货币数