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

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

3天内不再提示

动态规划详细指南(上)

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

算法技巧就那几个套路,如果你心里有数,就会轻松很多,本文就来扒一扒动态规划的裤子,形成一套解决这类问题的思维框架。废话不多说了,上干货。

动态规划问题的一般形式就是求最值 。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。

既然是要求最值,核心问题是什么呢? 求解动态规划的核心问题是穷举 。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值呗。

动态规划就这么简单,就是穷举就完事了?我看到的动态规划问题都很难啊!

首先,动态规划的穷举有点特别,因为这类问题 存在「重叠子问题」 ,如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。

而且,动态规划问题一定会 具备「最优子结构」 ,才能通过子问题的最值得到原问题的最值。

另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出 正确的「状态转移方程 才能正确地穷举。

以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。具体什么意思等会会举例详解,但是在实际的算法问题中, 写出状态转移方程是最困难的 ,这也就是为什么很多朋友觉得动态规划问题困难的原因,我来提供我研究出来的一个思维框架,辅助你思考状态转移方程:

明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case。

下面通过斐波那契数列问题和凑零钱问题来详解动态规划的基本原理。前者主要是让你明白什么是重叠子问题(斐波那契数列严格来说不是动态规划问题),后者主要集中于如何列出状态转移方程。

请读者不要嫌弃这个例子简单, 只有简单的例子才能让你把精力充分集中在算法背后的通用思想和技巧上,而不会被那些隐晦的细节问题搞的莫名其妙 。想要困难的例子,历史文章里有的是。

一、斐波那契数列

1、暴力递归

斐波那契数列的数学形式就是递归的,写成代码就是这样:

int fib(int N) {
    if (N == 1 || N == 2) return 1;
    return fib(N - 1) + fib(N - 2);
}

这个不用多说了,学校老师讲递归的时候似乎都是拿这个举例。我们也知道这样写代码虽然简洁易懂,但是十分低效,低效在哪里?假设 n = 20,请画出递归树。

PS:但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。

图片

这个递归树怎么理解?就是说想要计算原问题f(20),我就得先计算出子问题f(19)f(18),然后要计算f(19),我就要先算出子问题f(18)f(17),以此类推。最后遇到f(1)或者f(2)的时候,结果已知,就能直接返回结果,递归树不再向下生长了。

递归算法的时间复杂度怎么计算?子问题个数乘以解决一个子问题需要的时间。

子问题个数,即递归树中节点的总数。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n)。

解决一个子问题的时间,在本算法中,没有循环,只有 f(n - 1) + f(n - 2) 一个加法操作,时间为 O(1)。

所以,这个算法的时间复杂度为 O(2^n),指数级别,爆炸。

观察递归树,很明显发现了算法低效的原因:存在大量重复计算,比如f(18)被计算了两次,而且你可以看到,以f(18)为根的这个递归树体量巨大,多算一遍,会耗费巨大的时间。更何况,还不止f(18)这一个节点被重复计算,所以这个算法及其低效。

这就是动态规划问题的第一个性质: 重叠子问题 。下面,我们想办法解决这个问题。

2、带备忘录的递归解法

明确了问题,其实就已经把问题解决了一半。即然耗时的原因是重复计算,那么我们可以造一个「备忘录」,每次算出某个子问题的答案后别急着返回,先记到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。

一般使用一个数组充当这个「备忘录」,当然你也可以使用哈希表(字典),思想都是一样的。

int fib(int N) {
    if (N < 1) return 0;
    // 备忘录全初始化为 0
    vector<int> memo(N + 1, 0);
    // 初始化最简情况
    return helper(memo, N);
}

int helper(vector<int>& memo, int n) {
    // base case 
    if (n == 1 || n == 2) return 1;
    // 已经计算过
    if (memo[n] != 0) return memo[n];
    memo[n] = helper(memo, n - 1) + 
                helper(memo, n - 2);
    return memo[n];
}

现在,画出递归树,你就知道「备忘录」到底做了什么:

图片

实际上,带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。

图片

递归算法的时间复杂度怎么算? 子问题个数乘以解决一个子问题需要的时间。

子问题个数,即图中节点的总数,由于本算法不存在冗余计算,子问题就是f(1),f(2),f(3)f(20),数量和输入规模 n = 20 成正比,所以子问题个数为 O(n)。

解决一个子问题的时间,同上,没有什么循环,时间为 O(1)。

所以,本算法的时间复杂度是 O(n)。比起暴力算法,是降维打击。

至此,带备忘录的递归解法的效率已经和迭代的动态规划一样了。实际上,这种解法和迭代的动态规划思想已经差不多,只不过这种方法叫做「自顶向下」,动态规划叫做「自底向上」。

啥叫「自顶向下」? 注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说f(20),向下逐渐分解规模,直到f(1)f(2)触底,然后逐层返回答案,这就叫「自顶向下」。

啥叫「自底向上」? 反过来,我们直接从最底下,最简单,问题规模最小的f(1)f(2)开始往上推,直到推到我们想要的答案f(20),这就是动态规划的思路,这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算。

3、dp 数组的迭代解法

有了上一步「备忘录」的启发,我们可以把这个「备忘录」独立出来成为一张表,就叫做 DP table 吧,在这张表上完成「自底向上」的推算岂不美哉!

int fib(int N) {
    vector<int> dp(N + 1, 0);
    // base case
    dp[1] = dp[2] = 1;
    for (int i = 3; i <= N; i++)
        dp[i] = dp[i - 1] + dp[i - 2];
    return dp[N];
}

画个图就很好理解了,而且你发现这个 DP table 特别像之前那个「剪枝」后的结果,只是反过来算而已。实际上,带备忘录的递归解法中的「备忘录」,最终完成后就是这个 DP table,所以说这两种解法其实是差不多的,大部分情况下,效率也基本相同。

这里,引出「状态转移方程」这个名词,实际上就是描述问题结构的数学形式:

图片

为啥叫「状态转移方程」?为了听起来高端。你把 f(n) 想做一个状态 n,这个状态 n 是由状态 n - 1 和状态 n - 2 相加转移而来,这就叫状态转移,仅此而已。

你会发现,上面的几种解法中的所有操作,例如 return f(n - 1) + f(n - 2),dp[i] = dp[i - 1] + dp[i - 2],以及对备忘录或 DP table 的初始化操作,都是围绕这个方程式的不同表现形式。可见列出「状态转移方程」的重要性,它是解决问题的核心。很容易发现,其实状态转移方程直接代表着暴力解法。

千万不要看不起暴力解,动态规划问题最困难的就是写出状态转移方程 ,即这个暴力解。优化方法无非是用备忘录或者 DP table,再无奥妙可言。

这个例子的最后,讲一个细节优化。细心的读者会发现,根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1):

int fib(int n) {
    if (n == 2 || n == 1) 
        return 1;
    int prev = 1, curr = 1;
    for (int i = 3; i <= n; i++) {
        int sum = prev + curr;
        prev = curr;
        curr = sum;
    }
    return curr;
}

有人会问,动态规划的另一个重要特性「最优子结构」,怎么没有涉及?下面会涉及。斐波那契数列的例子严格来说不算动态规划,因为没有涉及求最值,以上旨在演示算法设计螺旋上升的过程。

下面,看第二个例子,凑零钱问题。

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

    关注

    19

    文章

    7488

    浏览量

    87855
  • 函数
    +关注

    关注

    3

    文章

    4327

    浏览量

    62573
  • 动态规划
    +关注

    关注

    0

    文章

    17

    浏览量

    8914
收藏 人收藏

    评论

    相关推荐

    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

    企业节能规划指南

    企业节能规划指南:节能:  指加强用能管理,采取技术可行、经济合理以及环境和社会可以承受的措施, 减少从能源生产到消费各个环节的损失和  浪费,更有效
    发表于 07-05 18:01 11次下载

    TSC动态补偿柜选型指南

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

    基于动态规划法的电力资源的合理分配

    通过实例在Matlab中展现了基于动态规划法,解决电力资源合理分配的问题,使得现实中电力资源的分配问题得到简化和程序化。结果显示,动态规划法在电力资源的合理分配问题上比较实用
    发表于 12-07 14:15 19次下载

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

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

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

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

    分层学习的自适应动态规划

    本文基于婴儿的认知发育模型LOC (Levels of Consciousness)提出了基于分层学习的自适应动态规划方法以改进学习和优化。根据LOC模型中感知的层次性以及工作目标的层次定义,为
    发表于 01-05 15:13 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 3198次阅读

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

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

    动态规划详细指南(下)

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