0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

动态规划详细指南(下)

jf_78858299 来源:labuladong 作者:labuladong 2023-04-19 10:25 次阅读

二、凑零钱问题

先看下题目:给你k种面值的硬币,面值分别为c1, c2 ... ck,每种硬币的数量无限,再给一个总金额amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。算法的函数签名如下:

// coins 中是可选硬币面值,amount 是目标金额
int coinChange(int[] coins, int amount);

比如说k = 3,面值分别为 1,2,5,总金额amount = 11。那么最少需要 3 枚硬币凑出,即 11 = 5 + 5 + 1。

你认为计算机应该如何解决这个问题?显然,就是把所有肯能的凑硬币方法都穷举出来,然后找找看最少需要多少枚硬币。

1、暴力递归

首先,这个问题是动态规划问题,因为它具有「最优子结构」。 要符合「最优子结构」,子问题间必须互相独立 。啥叫相互独立?你肯定不想看数学证明,我用一个直观的例子来讲解。

比如说,你的原问题是考出最高的总成绩,那么你的子问题就是要把语文考到最高,数学考到最高…… 为了每门课考到最高,你要把每门课相应的选择题分数拿到最高,填空题分数拿到最高…… 当然,最终就是你每门课都是满分,这就是最高的总成绩。

得到了正确的结果:最高的总成绩就是总分。因为这个过程符合最优子结构,“每门科目考到最高”这些子问题是互相独立,互不干扰的。

但是,如果加一个条件:你的语文成绩和数学成绩会互相制约,此消彼长。这样的话,显然你能考到的最高总成绩就达不到总分了,按刚才那个思路就会得到错误的结果。因为子问题并不独立,语文数学成绩无法同时最优,所以最优子结构被破坏。

回到凑零钱问题,为什么说它符合最优子结构呢?比如你想求amount = 11时的最少硬币数(原问题),如果你知道凑出amount = 10的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 1 的硬币)就是原问题的答案,因为硬币的数量是没有限制的,子问题之间没有相互制,是互相独立的。

那么,既然知道了这是个动态规划问题,就要思考 如何列出正确的状态转移方程

先确定「状态」 ,也就是原问题和子问题中变化的变量。由于硬币数量无限,所以唯一的状态就是目标金额amount

然后确定dp函数的定义 :函数 dp(n)表示,当前的目标金额是n,至少需要dp(n)个硬币凑出该金额。

然后确定「选择」并择优 ,也就是对于每个状态,可以做出什么选择改变当前状态。具体到这个问题,无论当的目标金额是多少,选择就是从面额列表coins中选择一个硬币,然后目标金额就会减少:

# 伪码框架
def coinChange(coins: List[int], amount: int):
    # 定义:要凑出金额 n,至少要 dp(n) 个硬币
    def dp(n):
        # 做选择,需要硬币最少的那个结果就是答案
        for coin in coins:
            res = min(res, 1 + dp(n - coin))
        return res
    # 我们要求目标金额是 amount
    return dp(amount)

最后明确 base case ,显然目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1:

def coinChange(coins: List[int], amount: int):

    def dp(n):
        # base case
        if n == 0: return 0
        if n < 0: return -1
        # 求最小值,所以初始化为正无穷
        res = float('INF')
        for coin in coins:
            subproblem = dp(n - coin)
            # 子问题无解,跳过
            if subproblem == -1: continue
            res = min(res, 1 + subproblem)

        return res if res != float('INF') else -1

    return dp(amount)

至此,状态转移方程其实已经完成了,以上算法已经是暴力解法了,以上代码的数学形式就是状态转移方程:

图片

至此,这个问题其实就解决了,只不过需要消除一下重叠子问题,比如amount = 11, coins = {1,2,5}时画出递归树看看:

图片

时间复杂度分析:子问题总数 x 解决每个子问题的时间

子问题总数为递归树节点个数,这个比较难看出来,是 O(n^k),总之是指数级别的。每个子问题中含有一个 for 循环,复杂度为 O(k)。所以总时间复杂度为 O(k * n^k),指数级别。

2、带备忘录的递归

只需要稍加修改,就可以通过备忘录消除子问题:

def coinChange(coins: List[int], amount: int):
    # 备忘录
    memo = dict()
    def dp(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:
            subproblem = dp(n - coin)
            if subproblem == -1: continue
            res = min(res, 1 + subproblem)

        # 记入备忘录
        memo[n] = res if res != float('INF') else -1
        return memo[n]

    return dp(amount)

不画图了,很显然「备忘录」大大减小了子问题数目,完全消除了子问题的冗余,所以子问题总数不会超过金额数 n,即子问题数目为 O(n)。处理一个子问题的时间不变,仍是 O(k),所以总的时间复杂度是 O(kn)。

3、dp 数组的迭代解法

当然,我们也可以自底向上使用 dp table 来消除重叠子问题,dp数组的定义和刚才dp函数类似,定义也是一样的:

dp[i] = x表示,当目标金额为i时,至少需要x枚硬币

int coinChange(vector<int>& coins, int amount) {
    // 数组大小为 amount + 1,初始值也为 amount + 1
    vector<int> dp(amount + 1, amount + 1);
    // base case
    dp[0] = 0;
    for (int i = 0; i < dp.size(); i++) {
        // 内层 for 在求所有子问题 + 1 的最小值
        for (int coin : coins) {
            // 子问题无解,跳过
            if (i - coin < 0) continue;
            dp[i] = min(dp[i], 1 + dp[i - coin]);
        }
    }
    return (dp[amount] == amount + 1) ? -1 : dp[amount];
}

图片

PS:为啥dp数组初始化为amount + 1呢,因为凑成amount金额的硬币数最多只可能等于amount(全用 1 元面值的硬币),所以初始化为amount + 1就相当于初始化为正无穷,便于后续取最小值。

三、最后总结

第一个斐波那契数列的问题,解释了如何通过「备忘录」或者「dp table」的方法来优化递归树,并且明确了这两种方法本质上是一样的,只是自顶向下和自底向上的不同而已。

第二个凑零钱的问题,展示了如何流程化确定「状态转移方程」,只要通过状态转移方程写出暴力递归解,剩下的也就是优化递归树,消除重叠子问题而已。

如果你不太了解动态规划,还能看到这里,真得给你鼓掌,相信你已经掌握了这个算法的设计技巧。

计算机解决问题其实没有任何奇技淫巧,它唯一的解决办法就是穷举 ,穷举所有可能性。算法设计无非就是先思考“如何穷举”,然后再追求“如何聪明地穷举”。

列出动态转移方程,就是在解决“如何穷举”的问题。之所以说它难,一是因为很多穷举需要递归实现,二是因为有的问题本身的解空间复杂,不那么容易穷举完整。

备忘录、DP table 就是在追求“如何聪明地穷举”。用空间换时间的思路,是降低时间复杂度的不二法门,除此之外,试问,还能玩出啥花活?

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 计算机
    +关注

    关注

    19

    文章

    6651

    浏览量

    84561
  • 函数
    +关注

    关注

    3

    文章

    3882

    浏览量

    61310
  • 动态规划
    +关注

    关注

    0

    文章

    17

    浏览量

    8880
收藏 人收藏

    评论

    相关推荐

    quartusII 详细使用指南

    quartusII 详细使用指南 应该有用
    发表于 04-28 09:24

    动态规划算法。

    动态规划算法资料。
    发表于 08-30 20:44

    运筹优化之动态规划解析

    运筹优化(七)--动态规划解析
    发表于 05-12 09:57

    LCS的动态规划算法

    LCS的动态规划算法(自底向上)
    发表于 05-25 15:06

    动态规划与贪婪法题的背包问题总结

    【LeetCode & 剑指offer刷题】动态规划与贪婪法题16:背包问题总结
    发表于 06-09 16:44

    centos7安全指南 精选资料分享

    侧重于红帽企业 Linux,但细节的概念和技术适用于所有Linux系统,该指南详细介绍了一些规划和工具,这些规划和工具可以为数据中心、工作场所以及家庭创建一个安全的计算环境。使用正确的
    发表于 07-28 08:51

    TSC动态补偿柜选型指南

    TSC动态补偿柜选型指南 Elspec是在国际上领先进行动态无功补偿和滤波的公司,在全球有4个工厂(以色列、葡萄牙、美国),总部位于
    发表于 03-24 17:27 1389次阅读

    基于动态规划的梯级泵站优化调度研究_专祥涛

    基于动态规划的梯级泵站优化调度研究_专祥涛
    发表于 01-21 12:16 0次下载

    基于联合双重概率矩阵的动态规划检测前跟踪算法_梁志兵

    基于联合双重概率矩阵的动态规划检测前跟踪算法_梁志兵
    发表于 03-22 09:20 0次下载

    基于时延Q学习的机器人动态规划方法

    机器人动态规划是指在某一个给定的运行空间中,移动机器人通过路径的动态规划来获得一条从初始位置到目标位置的最优路径。环境未知的情况下的机器人路径规划
    发表于 11-28 17:01 0次下载
    基于时延Q学习的机器人<b class='flag-5'>动态</b><b class='flag-5'>规划</b>方法

    求解含储能装置的微电网动态最优潮流的对偶半定规划方法

    法求解该问题,对孤岛运行的微电网动态最优潮流原始模型及向对偶半定规划模型的转换做了详细的介绍,并给出了严格的全局最优性判据。同时将储能装置的强非线性模型等价地变换成线性模型,并给出了相应的证明。某实际微电
    发表于 12-19 11:45 0次下载
    求解含储能装置的微电网<b class='flag-5'>动态</b>最优潮流的对偶半定<b class='flag-5'>规划</b>方法

    动态规划方法的利用matlab实现及其应用的有效工具详细资料概述

    本文运用 matlab 语言实现了动态规划的逆序算法,根据状态变量的维数,编写了指标函数最小值的逆序算法递归计算程序。两个实例的应用检验了该程序的有效性,同时也表明了该算法程序对众多类典型的动态
    发表于 06-14 08:00 5次下载
    <b class='flag-5'>动态</b><b class='flag-5'>规划</b>方法的利用matlab实现及其应用的有效工具<b class='flag-5'>详细</b>资料概述

    动态规划和递归有什么区别和联系

      前言 大家好,我是bigsai,好久不见,甚是想念(天天想念)! 很久前就有小伙伴被动态规划所折磨,确实,很多题动态规划确实太难看出了了,甚至有的题看了题解理解起来都费劲半天。
    的头像 发表于 11-16 17:27 2898次阅读

    国赛算法--动态规划详细资料

    动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20 世纪 50 年代初 R. E. Bellman
    发表于 11-24 09:57 0次下载

    动态规划详细指南(上)

    动态规划问题的一般形式就是求最值 。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。
    的头像 发表于 04-19 10:25 344次阅读
    <b class='flag-5'>动态</b><b class='flag-5'>规划</b><b class='flag-5'>详细</b><b class='flag-5'>指南</b>(上)