从八卦的递归到动态规划

2020-10-09  本文已影响0人  0843d07b95d5

题外话:

好久没写博客了,主要觉得自己写的不怎么样,肚子没墨水了。今天又出来榨干肚子里的最后一滴墨水。

进入正题:

前面写过两篇动态规划的题目,今天又遇到动态规划了,想到了一个更容易理解的方法,我们用凑硬币这道题目来加深理解。网络上这个题目已经被写烂了,大多数文章都是复制粘贴的。本文原创。开始讲解之前读者需要了解递归。本文从递归解法讲起直到推出动态规划方法。

问题描述:

(凑硬币问题)给你 k 种面值的硬币,面值分别为 c1, c2 ... ck ,每种硬币的数量无限,
再给一个总金额 amount ,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回-1
这里我们k = 3 , c1 = 1、c2 = 3、c3 = 5.

1. 递归解法:

形象的理解递归
比如f是一个八卦的人,有一天a和b谈恋爱了,只告诉了c。 f想知道a和b的关系, 就去问e, e说他自己不知道可以帮忙去问d,d说他也不知道可以帮忙去问c(递归调用过程),c呢就告诉了d,d又告诉了e,e告诉了f(回溯过程,回溯过程可以看作答案一步步传到f耳朵里的过程)。
用递归的思想来思考该问题大致如下
我们知道amount=0和amount<0基本情况(c知道a和b的关系),求amount=11的答案(f想知道a和b的关系),ans(11)=ans(11-5)+1 or ans(11)= ans(11-3)+1 or ans(11)= ans(11-1)+1 (f去问了他认识的人e。如果f认识d或者直接认识c他可以更快的知道答案)。递归的回溯过程程序会自动完成。

def colCoins_rec(coins, amount):
    """
    递归解法
    :param coins: 硬币面值
    :param amount: 总金额
    :return: 凑齐总金额的最少硬币数量
    """
    # 基本情况(c知道的情况)
    if amount == 0:
        return 0
    if amount < 0:
        return -1
    
    res = float("inf")
    # 有三种面值的硬币可以选择(f认识的人,因为f只认识e所以只有一个选择)
    for coin in coins:
        #选择(f选择问谁,交给谁来获取答案)
        subproblem = colCoins_rec(coins, amount-coin)
        # 子问题无解,(f问到了错误地人g,g是个孤儿谁都不认识,走到了死胡同,f要问下一个人)
        if subproblem < 0: continue
        # 选择最优的答案(如果f认识c,那么他很快可以获取答案,就可以选择这条信息链)
        res = min(res, 1+colCoins_rec(coins, amount-coin))
    return res

递归方法存在什么问题呢?很明显的一个是使用了函数调用栈,栈在计算机中是一个相对很有限的资源。还有一个是存在重叠子问题,导致了时间复杂度大大增加。LeetCode上这个方法一般是无法通过的。重叠子问题是什么呢?

                    h                  存在直接连线的表示互相认识
                 /     \               如果h想从c那里获得c知道的答案那么他要问认识的f,g同样
                f       g               f,g要问他们认识的人。
              /  \    /  \            观察发现f,e,g被问了多次!这是导致算法复杂度高的主要原因
            g    e   e   f
          /  \   ........
        e     f
      /  \
    c    d

如何解决上述存在的重叠子问题呢?答案是使用备忘录方法。

2备忘录递归方法

备忘录方法是这些人共用一个列表的,如果谁已经知道答案就填在列表对应的元素里里,每个人在问之前先访问列表,如果得到大难就不用再继续问下去了,避免重复的被问。

mem = [-1]*15
def colCoins_rec_mem(coins, amount, mem):
    """
    在递归的解法的基础上加上备忘录解决重叠子问题。
    :param coins: 硬币面值
    :param amount: 总金额
    :param mem: 备忘录
    :return: 最少硬币数量
    """
    # 基本情况(c知道的答案)
    if amount < 0:
        return -1
    if amount == 0:
        return 0
    # 查看备忘录,看看是否计算过(是否被问过)
    if mem[amount] != -1:
        return mem[amount]
    # 选择(询问的过程)
    res = float("inf")
    for coin in coins:
        subproblem = colCoins_rec_mem(coins, amount-coin, mem)
        if subproblem < 0: continue
        res = min(res, 1+colCoins_rec_mem(coins, amount-coin, mem))
        # 得到答案后写进备忘录
        mem[amount] = res
    return res

我们可以看到基于备忘录的递归算法,会在计算(询问)前去查看备忘录,避免重复计算。而且代码与纯递归方法也非常的相似。只多了查看备忘录的过程和获得答案后写进备忘录的操作。这个备忘录解决了重叠子问题,实际上他还是递归方法。我们可以看到其实备忘录记录的就是子问题和原问题的答案,递归方法是自顶向下的以“询问”的方法求得答案,那么可不可以让c主动的向他认识的人扩散答案直到让f知道答案。这个自底向上的“传播”就是动态规划!(好烦c这种人啊,就像我很烦动态规划题目)话不多说看代码

def colCoins_mem(coins, amount):
    """
    自底向上递推出答案
    :param coins:  硬币面值
    :param amount: 总金额
    :return:
    """
    # 构造一个备忘录
    mem = [0]*(amount+1)  # mem[i]表示凑出金额i需要的最少硬币的数量
    # 基本情况(这句代码可以不要)
    mem[0] = 0
    # 开始推导(c开始传播八卦了)
    for i in range(1, amount+1):
        # 从c开始向他认识的人传播消息
        for coin in coins:
            res = i - coin
            if res >= 0:
                mem[i] = mem[res] + 1
            else:
                continue
    return mem[amount]

总结:

我们看看动态规划的要素就是:1、基本情况(知道答案的那个人)2、明确mem里面存储的是什么我们也可以称作状态。3、选择(每个状态有多少转移方方式)4、明确状态转移的细节(状态转移方程)。该文章一气呵成有什么不懂的留言,有空改一下。动态规划就到这里了。

上一篇下一篇

猜你喜欢

热点阅读