CoinChange问题(上)——DP&母函数

CoinChange问题

CoinChange问题是很经典的问题,在leetcode上有518. Coin Change 2 h和322. Coin Change 。其中:

  • Coin Change 2:给定固定有限种类 a1,a2,...,am 的硬币,求换成金额 n 的方法数,即求不定方程mi=1aiki=n的非负解的个数。
  • Coin Change:上述问题的变体,类似完全背包问题,一个最优化问题:
    mini=1mkis.t.a1k1+a2k2+...+amkm=n

下面将主要以Coin Change 2来讨论,首先想到的是直接从解的角度出发:

不定方程的解角度

首先看的是解的存在性,对于不定方程 mi=1aiki=n 的解的存在性可以由下定理保证:(证明在这里)

定理1:不定方程 mi=1aiki=n 存在解的充要条件是 gcd(a1,a2,...,am)|n .

实际上,从不定方程的解的角度入手是基本上解决不了本问题的,以二元一次不定方程为例,即 ax+by=n ,假设它有特解 (x0,y0) ,那么方程的解有形式:

x=x0bgcd(a,b)ty=y0+agcd(a,b)t

这样,非负整数解一下就可以求出来了,但是问题是要首先得到特解,多元一次不定方程的解也有类似的形式,而特解没有足够好的办法得到,所以这种方法不太好。

母函数角度

高中时代就看过一种巧妙的组合大法,甚至有某科大巨犇说:一切组合问题都是可以由母函数生成。我一脸懵逼地深以为然。以 x+2y+5z=5 为例,设:

f1=1+x+x2+x3+...f2=1+x2+x4+...f3=1+x5+x10+...

那么我们要求的方法总数即为多项式 f1f2f3 x5 项的系数,稍微想一想就知道这个操作的准确性,如果我们有强大的工具如MATLAB,那么我们可以考虑 f1f2f3 在0处的Maclaurin展开,处理一下:
f(x)=f1f2f3=11x11x211x5

然后剩下的就靠MATLAB求导数什么的了,稳。但白手起家的编程的话,略繁。还有一些类似问题的思路可以看 这里

DP角度

下面依然会以 x+2y+5z=5 为例,用 dp(i,j) 表示目标金额为 i ,且用前j种硬币的方法数。这里的逻辑是这样的,对于不定方程 x+2y+5z=5 ,考虑 z ,要么z=0要么 z1 ,这两种情况分别对应于 x+2y=5 x+2y+5z=0 的解的个数,所以,我们可以得到状态转移方程:

dp(i,j)={dp(i,j1)+dp(icoins[j],j),icoins[j]dp(i,j1),i<coins[j]

这里写图片描述
作图所示为整个过程的示意图,其中最后的 dp(5,5) 即为所求,代码如下:

class Solution(object):
    def change(self, amount, coins):
        dp=[[0]*(len(coins)+1) for x in range(amount+1)]
        #初始化
        for y in range(amount+1):
            dp[y][0]=0
        for x in range(len(coins)+1):
            dp[0][x]=1
        #dp[i][j]=dp[i][j-1]+dp[i-conins[j]][j]
        for i in range(amount):
            for j in range(len(coins)):
                tempi=i+1
                tempj=j+1
                if tempi>=coins[tempj-1]:
                    dp[tempi][tempj]=dp[tempi][tempj-1]+dp[tempi-coins[tempj-1]][tempj]
                else:
                    dp[tempi][tempj]=dp[tempi][tempj-1]                  
        return dp[-1][-1]

实际上这样的方法是灰常消耗空间的,注意到实际上,每次进行状态转移的时候,对于某个元,我们需要的元素是左边一格的元素和上方的一个元素,所以说我们完全可以一列一列地计算,而仅仅用一个一维数组即可,计算时间大大减少(虽然时间复杂度相同),红框用的是下方代码,蓝框用的是上方代码。代码如下:
这里写图片描述

class Solution(object):
    def change(self, amount, coins):
        dp = [0] * (amount + 1)
        dp[0] = 1
        for c in coins:
            for x in range(c, amount + 1):
                dp[x] += dp[x - c]
        return dp[amount]

关于 Coin Change问题

这部分的内容将在下章中具体探讨,包括与背包问题的关系,算法上探讨一下BFS,DFS和各种启发式算法。下面将先给出其状态转移方程:

dp(i,j)={min{dp(i,j1),1+dp(icoins[j],j)},icoins[j]dp(i,j1),i<coins[j]

过程如上右图所示,扔下代码就跑:

class Solution(object):
    def coinChange(self, coins, amount):
        temp=233333333333333333333
        dp=[[0]*(len(coins)+1) for x in range(amount+1)]
        for x in range(amount+1):
            dp[x][0]=temp
        for i in range(amount):
            for j in range(len(coins)):
                tempi=i+1
                tempj=j+1
                if tempi>=coins[tempj-1]:
                    dp[tempi][tempj]=min(dp[tempi][tempj-1],1+dp[tempi-coins[tempj-1]][tempj])
                else:
                    dp[tempi][tempj]=dp[tempi][tempj-1]
        if dp[-1][-1]==temp:
            return -1
        return dp[-1][-1]

或者用下面这更为高效的代码(一维数组杀)

class Solution(object):
    def coinChange(self, coins, amount):
        dp=[233333333]*(amount+1)
        dp[0]=0
        for c in coins:
            for i in range(c,len(dp)):
                dp[i]=min(dp[i],dp[i-c]+1)
        if dp[-1]==233333333:
            return -1
        return dp[-1]

敬请期待:CoinChange问题(下)——背包问题

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值