目录
1 动态规划是什么
2 如何判断一个问题可以使用动态规划来解决
3 判断要解决问题在动态规划经典问题中的分类
4 动态规划的套路
5 如何进行优化
6 案例分析
1. 动态规划是什么
动态规划(Dynamic programming,DP)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划没有实际的步骤来规定第一步做什么第二步做什么。所以更加确切的说,动态规划是一种解决问题的思想。这种思想的本质是,一个规模比较大的问题,是通过规模比较小的若干问题的结果来得到的(通过取最大,取最小,或者加起来之类的运算)。
大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
动态规划一般是比较高效,那么相对低效的是什么呢?暴力搜索!动态规划相对暴力搜索的优点在于去掉了重复的计算量,可以通过直接获取之前计算的结果来避免重复计算,这是动态规划算法高效的重要原因。之前计算的结果都储存在表格中,也就是动态规划表。
动态规划与递归可以对应起来理解,前者是自底而上进行,后者是自顶而下进行搜索。同一个问题可能既可以用动态规划解决,也可以使用递归解决。
动态规划的核心代码量通常在几行到几十行,包含状态定义,状态初始化,状态转移方程三大主要部分。
2. 如何判断一个问题可以使用动态规划来解决
发现一个问题适合用动态规划解决非常重要,能坚定信念去找状态转移方程!
需要满足两个重要的特点。存在最优子结构和重叠性
最优子结构:如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。(配题目来理解最优子结构)
重叠性:子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。(配图对比自顶而下和自下而上,及帮助理解重叠性)
根据题目要求的答案,1. 求最大值/最小值2. 求可不可行3. 求方案总数,就有较大概率使用动态规划。
3. 判断要解决问题在动态规划经典问题中的分类
动态规划问题种类的判断正确(如判断出是某个经典问题或者是其变种),有助于快速分析出,状态的定义和状态转移方程。
动态规划问题的分类,一般可以按照问题中状态的定义来分的,一维,双一维,二维,区间,背包,划分型。
动态规划比较经典的问题种类有,
递推-斐波拉契数列,爬楼梯,
背包问题
最长递增子序列LIS
最长公共子序列LCS
最小路径和
最多路径数
4. 动态规划的套路
套路是固定的,分为4个部分。1定义状态2推导状态转移方程3初始化状态4问题要求的最后答案是什么
(1)状态是什么,如何找到状态
首先判断状态是哪一种?一维,双一维,二维,区间,划分型。有两个难点。1是不容易看出来需要使用哪一种状态。2是不容易定义,前i个元素,还是以第i个元素结尾的序列等。较简单的比较容易能看得出来,有些题目不是那么容易看的出来的。一般而言,题目求什么,就定义状态是什么。如果不容易定义,可以根据属于哪一类经典问题,参考进行定义。
(2)如何找状态转移方程
可以使用数学证明中的归纳法!
F[n]的定义
动态规划的公式其实不难推,就那么几种(下面四种)。如果这几种套上去都搞不定,那么八成F[n]需要重新定义。
定义完毕,就要找F[n]和前面n步的关系了,分成四种,挨个套就行,难度由从低到高
a, F[n]跟前面一两步有关
这种简单
b, F[n]跟前面n步都有关
这种找到公式也不难,但要注意时间优化了,不优化往往就n^2了,举例子《最长上升子序列》,代码实现的过程中就需要借助for循环遍历前面所有的状态。
c, F[n]需要细分
这种情况下,你发现看着像,但是跟前面n步的关系整的你头疼。
举个例子,股票的几个中等难度的题就是这样。
那就把F[n]分成 F1[n] F2[n],其实就是双一维类型
d, F[n],一维数组不够用了
F[n]的细分其实是这个的简单情况。
举个例子,股票的状态有出售,买入。那么F[n]再加一维数组,变成 F[n][],第二纬只有0,1即可。二维比较难的,二维里面还区分划分型。
对于二维的状态方程,使用dp表更加简洁明了,在对dp表进行初始化后,根据表格进行分析,更加容易找到规律。
在代码实现的过程中,i起始的取值从1还是2开始,取决于等式右边的索引不能为负,如果只有dp[i-1],那可以从1开始;如果有dp[i-2]则只能从2开始。
如果当前状态与之前的所有状态都有关,就会有两层循环,内层循环要遍历之前的所有的状态
(3)如何进行初始化
对于一维,通常会是dp[0],dp[1]可能要初始化,初始化的目的是为了保证dp[i]的索引不出现负数。状态转移方程dp[i]=……,等式右侧中出现的如果只有dp[i-1],则dp[0]初始化就行(索引i-1=0,i从1开始取),dp[1]可以推导出来;如果等式右侧出现了dp[i-2],则需要初始化dp[0],dp[1](索引i-2=0,i=2,最小可以推导的是dp[2])。
对于二维,可能需要对dp[0][j],和dp[i][0],即第一行和第一列进行初始化。同样,如果出现了dp[i-2]的情况,也是需要对第二行和第二列进行初始化,不过一般都是只用初始化第一行和第一列。
在初始化的过程中,dp的长度也是个细节,有时与给定数组的长度相同,有时需要比给定数组的长度多一。这个是因为状态方程中出现了dp[i+1],为了使数组索引时不越界,要使得其长度+1.
有的状态初始化其实在定义dp的时候就已经完成了初始化,没有专门的初始化代码。
从状态转移方程的下标思考初始化状态,注意数组的下标不能越界(上界和下界),或者思考是否可以通过给状态数组多加样或一列,从而避免复杂的初始化讨论!
(4)问题的答案
如果状态的定义就是答案,通常就是dp[n],如果不是就要根据具体的问题进行分析了,譬如有的是dp数组元素之和等等。
5. 如何进行优化
动态规划通常进行储存空间上的优化,这种优化思想实际上是滚动数组(可以插入图片)。
如果一维dp中,当前状态仅与某几个状态有关,则可以用变量来记录相关状态,空间复杂度降为O(1)。二维dp中,当前状态仅与某几行或者某几列有关,则可以用一维数组来记录相关行列的状态,空间复杂的杜降为O(n)(原来的空间复杂度是O(n*n))。
假如状态转移方程如下:f[i][j] = f[i - 1][j] + f[i][j - 1]。我们可以发现,第i层的状态,已经和第i-2层的状态没有关系了,那么这种情况下,用于存储第i-2层的空间就可以被重复利用。方法非常简单,把数组的第一维对2取模就可以了:f[i % 2][j] = f[(i - 1) % 2][j] + f[i % 2][j-1]。这种方法通常可以将空间复杂度降低一个数量级。(如果用一维数组的话,状态转移公式的F(n)要分成2个,循环滚动)
6. 案例分析
案例是Leetcode上的题目,内容较多,在此博文中进行总结
主要帮助理解最优子结构和重叠性,分析问题结构看出可以采用动态规划算法
理解状态定义
理解如何找状态转移方程。
参考
动态规划是什么
https://blog.csdn.net/cc_again/article/details/25866971
【干货】动态规划十问十答
https://zhuanlan.zhihu.com/p/26743197
状态方程推导
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/solution/dong-tai-gui-hua-shen-ru-fen-xi-by-wang-yan-19/
告别动态规划,连刷 40 道题,我总结了这些套路,看不懂你打我(万字长文)
https://zhuanlan.zhihu.com/p/91582909?utm_source=cn.ticktick.task&utm_medium=social&utm_oi=684404101006102528
动态规划的思维导图
https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/
背包九讲
https://blog.csdn.net/stack_queue/article/details/53544109