python算法之动态规划讲解

初识动态规划

在将动态规划之前,我们来继续深入了解以下递归,这样有利于我们对动态规划的了解,
我们还是以斐波那契数列为例,这里我们已经写下了如下的代码:

def x(n):
    if n == 0:
     return 0
    elif n == 1:
        return 1
    else:
        return x(n-1) + x(n-2)

但是我们会发现每个问题被我们分解成为了两个问题,但是我们想一想?这时候时间复杂度不就变成了2**n,这会导致计算十分费时间,我们可以尝试以下计算第50个数,会发现它会计算很长时间,有可能还没有计算出来,这时候我们就可以进行优化。
我们思考一下,我们在计算中会计算很多重复的数,x(n-2)就会计算两次,后面的计算次数是不是会更多?我们可以用字典将每个数据进行储存,储存完成减少了重复计算了

memo = {
     }
def fib1(n):
    if n in memo:
        return memo[n]
    else:
        if n <= 2:
            f = 1
        else:
            f = fib1(n-1) + fib1(n-2)
        memo[n] = f
        return f

我们这时候就利用了字典提高了递归效率,时间复杂度只有n,但是我们这是自上向低的计算,我们可以换一种计算方式,让他实现自底向上的计算:

def fib2(n):
    fib = {
     }
    for k in range(n+1):
        if k <= 2:
            f = 1
        else:
            f = fib[k-1] + fib[k-2]
        fib[k] = f
    return fib[n]

这里发现我们根本没有用到了递归,这里时间复杂度也是n。
动态规划,就是以这种方式,找到了类似f = fib[k-1] + fib[k-2]这种等式(表达式),然后进行我们的计算,这里就需要我们分析问题找到表达式了。

细致讲解

很多计算机被用于优化某些值,如两点之间找到最短路径,计算机科学家采用很多策略来解决这些问题,动态规划就是策略的其中之一。
我们以一道经典的例题来讲解以下动态规划。
在找零的时候使用最少的硬币。假设莫格自动售货机制造商希望每笔交易中给出最少的硬币,一个顾客在使用一美元的纸币购买了价值37美分的物品,最少该找多少硬币呐?答案是六枚,25美分的两枚,10美分的一枚,1美分的3枚。
如何计算呐?从面值最大25美分开始,使用尽可能多的硬币,然后尽可能使用第二大的硬币,这种方法叫做贪婪算法,试图最大程度的解决问题。
我们尝试一种更优的解法我们来先看一下递归解法,首先确定基本情况,如果要找的零钱金额与硬币的面值相同,那么只需要找1枚硬币即可。
如果要找的零钱的金额与硬币面值不相同,则有多种选择,一枚一分的硬币加上找零金额减去1之后所需的金额,一枚五分的硬币…等等
我们出示以下代码:

def recMC(coinValueList, change):
    minCoins = change
    if change in coinValueList:
        return 1
    else:
        for i in [c for c in coinValueList if c <= change]:
            numCoins = 1+recMC(coinValueList, change-i)
            if numCoins < minCoins:
                minCoins = numCoins
    return minCoins

但是他的效率十分低,我们针对63分的情况,要计算67716925次递归才能完成。其中主要因为我们将大量时间和资源浪费到了重复计算中了
python算法之动态规划讲解_第1张图片

如果我们可以用查询表后,似乎可以减少一点计算量。

def recDC(coinValueList,change, knowResults):
    minCoins = change
    if change in coinValueList:
        knowResults[change] = 1
        return 1
    elif knowResults[change] > 0:
        return knowResults[change]
    else:
        for i in [c for c in coinValueList if c <= change]:
            numCoins = 1+recMC(coinValueList, change-i, knowResults)
            if numCoins < minCoins:
                minCoins = numCoins
                knowResults[change] = minCoins
    return minCoins

这里我们把递归调用降低到了221次,但是我们发现它并不是太正规,如果查看knowResults表,会发现其中有一些空白的地方,我们做的优化并不是动态规划,而是通过记忆(或者叫缓存)的方法来优化程序新跟那个,但是仍然需要计算221次,
我们来尝试用动态规划进行解决我们来看找零11分硬币,我们从一分开始计算
python算法之动态规划讲解_第2张图片

上面的图片为我们进行了展示,11分的情情况如下图
python算法之动态规划讲解_第3张图片
我们有三个可选方案。
1.一枚一分的加上十分的零钱最少需要的硬币(11-1)
2.一枚五分的硬币加上六分零钱最少需要的硬币(11-5)
3.一枚十分的加上找一分零钱最少需要的硬币(11-10)
这里第一个和第三个均为最优解,即需要两枚硬币。
函数如下

#动态规划解法,函数接受三个值,硬币面值列表,找零金额,以及由每个找零金额所需要的最少硬币数构成的列表,自底向下的计算
def dpMakeChange(coinValueList, change, minCoins):
    for cents in range(change+1):
        coinCount = cents
        for j in [c for c in coinValueList if c <= cents]:
            if minCoins[cents-j]+1 < coinCount:
                coinCount = minCoins[cents-j] -1
        minCoins[cents] = coinCount
    return minCoins[change]
#dp是动态规划的简写

我们从第四行开始的循环针对有cents指定的找零金额考虑所有可用的面值,我们采用自底向上的方法进行计算。
尽管找零算法再寻找最少硬币数时表现出色,但是没有记录所以用硬币,所以并不能解决我们的实际问题,我们可以尝试通过记录minCoins表中每一项
所加的硬币,可以轻松拓展函数,并通过它知晓之前所加的硬币。
代码如下:

def dpMakeChange(coinValueList, change, minCoins,coinsUsed):
    for cents in range(change+1):
        coinCount = cents
        newCoin = 1
        for j in [c for c in coinValueList if c <= cents]:
            if minCoins[cents-j]+1 < coinCount:
                coinCount = minCoins[cents-j] -1
                newCoin = j
        minCoins[cents] = coinCount
        coinsUsed[cents] = newCoin
    return minCoins[change]
def printCoins(coinsUsed, change):
    coin = change
    while coin > 0:
        thisCoin = coinsUsed[coin]
        print(thisCoin)
        coin -= thisCoin

这里所有问题我们都已经解决完成了。我们可以进行一下简单的实践,

应用实践

https://leetcode-cn.com/problems/find-the-minimum-number-of-fibonacci-numbers-whose-sum-is-k/

你可能感兴趣的:(算法,python,python,算法)