In computer science, mathematics, management science, economics and bioinformatics, dynamic programming (also known as dynamic optimization) is a method for solving a complex problem by breaking it down into a collection of simpler subproblems, solving each of those subproblems just once, and storing their solutions. (动态规划是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。)
The next time the same subproblem occurs, instead of recomputing its solution, one simply looks up the previously computed solution, thereby saving computation time at the expense of a (hopefully) modest expenditure in storage space. (Each of the subproblem solutions is indexed in some way, typically based on the values of its input parameters, so as to facilitate its lookup.) The technique of storing solutions to subproblems instead of recomputing them is called “memoization”.
当再次遇到子问题时,只需查找之前的解而无须再次计算,即牺牲适当的存储换取计算时间的节省。(子问题的解要以一定方式索引,通常基于输入参数以方便查找)。这种存储子问题解而非再次计算一遍的技术即记忆化。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
Dynamic programming algorithms are often used for optimization.A dynamic programming algorithm will examine the previously solved subproblems and will combine their solutions to give the best solution for the given problem.
常用于解决最优化问题,DP通过组合子问题的最优解得出最优解。英文维基中举了找零钱的例子,即找到最少数量的硬币组合成某数额(amount)的零钱数(给定一些硬币面值),也是一种背包问题
a dynamic programming algorithm would find an optimal solution for each amount by first finding an optimal solution for each smaller amount and then using these solutions to construct an optimal solution for the larger amount(即先找小额度的最优解,由这些小额度的最优解构造大额度的最优解)
并且与贪心算法做了比较:
In contrast, a greedy algorithm might treat the solution as a sequence of coins, starting from the given amount and at each step subtracting the largest possible coin denomination that is less than the current remaining amount. If the coin denominations are 1,4,5,15,20 and the given amount is 23, this greedy algorithm gives a non-optimal solution of 20+1+1+1, while the optimal solution is 15+4+4.(贪心算法把解当成硬币的序列,每一步都减去当前最大面额的硬币,但得到的并不是真正的最优解。)
In addition to finding optimal solutions to some problems, dynamic programming can also be used for counting the number of solutions.(除了求最优解,还可以用于求解解的数量。)for example counting the number of ways a certain amount of change can be made from a given collection of coins, or counting the number of optimal solutions to the coin change problem described above.(比如可以组成指定零钱额度的硬币一共有多少种组合方式,以及最优组合有多少种方式。)
DP既是数学优化方法又是计算机编程方法,以递归的形式实现。
知乎:什么是动态规划
动态规划的本质,是对问题状态的定义和状态转移方程的定义
这样的解释一看上去好像跟分解成子问题没什么关系啊
动态规划是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决
这样倒是联系了子问题,状态和递推这些概念
如何拆分问题,才是动态规划的核心。而拆分问题,靠的就是状态的定义和状态转移方程的定义
进一步的解释
动态规划适用的情形有下面几种
1. 最大值/最小值
2. 有无可行解
3. 求方案个数(如果需要列出所有方案,则一定不是DP,因为全部方案为指数级别复杂度,所有方案需要列出时往往用递归)
4. 给出的数据不可随便调整位置
常见的用DP算法的问题:
1. 斐波那契数列
2. 最长子序列
3. 背包问题
4. 股票投资类问题
5. 棋盘类问题
还是取他山之石,解决此类问题分两步走:
1. 确定状态
2. 根据状态列状态转移方程
动态规划问题中一般从以下四个角度考虑:
- 状态(State)
- 状态间的转移方程(Function)
- 状态的初始化(Initialization)
- 返回结果(Answer)
给定一个数列,长度为N,求这个数列的最长上升(递增)子数列(LIS)的长度. 以1 7 2 8 3 4为例。
这个数列的最长递增子数列是 1 2 3 4,长度为4;次长的长度为3, 包括 1 7 8; 1 2 3 等.
(此处借用了知乎回答的例子)首先来定义问题和其子问题:
给定一个数列,长度为N,
设 Fk 为:以数列中第k项结尾的最长递增子序列的长度. 求 F1..FN 中的最大值.
对于 Fk 来说, F1...Fk−1 都是它的子问题:因为以第 k 项结尾的最长递增子序列(下称LIS),包含着以第 1..k−1 中某项结尾的LIS。而此处的 Fk 就是状态。对状态的定义或者以什么状态并不是唯一的。
上述状态定义好之后,状态和状态之间的关系式,就叫做状态转移方程
仍以上述的LIS问题为例,设 A 为题中数列,状态转移方程为:
F1=1 (根据状态定义导出边界情况)
Fk=max(Fi+1|Ak>Ai,i∈(1..k−1))(k>1)
这里的状态转移方程,就是定义了问题和子问题之间的关系。
可以看出,状态转移方程就是带有条件的递推式。
上述的状态转移方程中,等式右边不会用到下标大于左边i或者k的值,这是”无后效性“的通俗上的数学定义,符合这种定义的状态定义,我们可以说它具有“最优子结构”的性质,在动态规划中我们要做的,就是找到这种“最优子结构”。在对状态和状态转移方程的定义过程中,满足“最优子结构”是一个隐含的条件(否则根本定义不出来)。关于无后效性见下面的解释。
另一个答主王勐也给出了一个斐波那契的例子来说明状态,状态转移和递推的问题:
要计算第100个非波那契数,每一个非波那契数就是这个问题的一个状态,每求一个新数字只需要之前的两个状态。所以同一个时刻,最多只需要保存两个状态,空间复杂度就是常数;每计算一个新状态所需要的时间也是常数且状态是线性递增的,所以时间复杂度也是线性的。
上面这种状态计算很直接,只需要依照固定的模式从旧状态计算出新状态就行(a[i]=a[i-1]+a[i-2]),不需要考虑是不是需要更多的状态,也不需要选择哪些旧状态来计算新状态。对于这样的解法,我们叫递推。
还是来自王勐答主的回答
(https://www.zhihu.com/question/23995189/answer/35429905)
所谓阶段是指随着问题的解决,在同一个时刻可能会得到的不同状态的集合。非波那契数列中,每一步会计算得到一个新数字,所以每个阶段只有一个状态。想象另外一个问题情景,假如把你放在一个围棋棋盘上的某一点,你每一步只能走一格,因为你可以东南西北随便走,所以你当你同样走四步可能会处于很多个不同的位置。从头开始走了几步就是第几个阶段,走了n步可能处于的位置称为一个状态,走了这n步所有可能到达的位置的集合就是这个阶段下所有可能的状态。
假如问题有n个阶段,每个阶段都有多个状态,不同阶段的状态数不必相同,一个阶段的一个状态可以得到下个阶段的所有状态中的几个。那我们要计算出最终阶段的状态数自然要经历之前每个阶段的某些状态
好消息是,有时候我们并不需要真的计算所有状态,比如这样一个弱智的棋盘问题:从棋盘的左上角到达右下角最短需要几步。答案很显然,用这样一个弱智的问题是为了帮助我们理解阶段和状态。
某个阶段确实可以有多个状态,正如这个问题中走n步可以走到很多位置一样。但是同样n步中,有哪些位置可以让我们在第n+1步中走的最远呢?没错,正是第n步中走的最远的位置。换成一句熟悉话叫做“下一步最优是从当前最优得到的”。所以为了计算最终的最优值,只需要存储每一步的最优值即可,解决符合这种性质的问题的算法就叫贪心。如果只看最优状态之间的计算过程是不是和非波那契数列的计算很像?所以计算的方法是递推。
既然问题都是可以划分成阶段和状态的。这样一来我们一下子解决了一大类问题:一个阶段的最优可以由前一个阶段的最优得到。如果一个阶段的最优无法用前一个阶段的最优得到呢?(如果只需要之前两个阶段就可以得到当前最优,那跟只用之前一个阶段并没有本质区别。)最麻烦的情况在于你需要之前所有的情况才行。
再来一个迷宫的例子。在计算从起点到终点的最短路线时,你不能只保存当前阶段的状态,因为题目要求你最短,所以你必须知道之前走过的所有位置。因为即便你当前再的位置不变,之前的路线不同会影响你的之后走的路线。这时你需要保存的是之前每个阶段所经历的那个状态,根据这些信息才能计算出下一个状态!每个阶段的状态或许不多,但是每个状态都可以转移到下一阶段的多个状态,所以解的复杂度就是指数的,因此时间复杂度也是指数的。
此处提到的之前的路线会影响到下一步的选择,这个令人不开心的情况就叫做有后效性。刚刚的情况实在太普遍,解决方法实在太暴力,有没有哪些情况可以避免如此的暴力呢?契机就在于后效性。
有一类问题,看似需要之前所有的状态,其实不用。不妨也是拿最长上升子序列的例子来说明为什么他不必需要暴力搜索,进而引出动态规划的思路。
以LSI的问题为例,在问题的第i个阶段,决定是否要选择第i个数,第i个阶段有两个状态,分别是选和不选。但选择的时候只需要和之前选定的一个数字比较就行了(而不是所有的数),这样就不用记录所有的状态。
虽然我们不在乎某序列之前都是什么元素,但我们还是需要这个序列的长度的。所以我们只需要记录以某个元素结尾的LIS长度就好!因此第i个阶段的最优解只是由前i-1个阶段的最优解得到的,然后就得到了DP方程:
LIS(i)=max(LIS(j)+1),j<i,a[j]<a[i]
所以一个问题是该用递推、贪心、搜索还是动态规划,完全是由这个问题本身阶段间状态的转移方式决定的!
每个阶段 只有一个状态-> 递推;
每个阶段的最优状态都是由 上一个阶段的最优状态得到的-> 贪心;
每个阶段的最优状态是由之前 所有阶段的状态的组合得到的-> 搜索;
每个阶段的最优状态可以从之前某个阶段的 某个或某些状态直接得到而不管之前这个状态是如何得到的->动态规划。
“每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到”这个性质叫做最优子结构;
“而不管之前这个状态是如何得到的”这个性质叫做无后效性
纵观这个问题下的回答,很多人都是解决了很多DP类问题之后的总结,实际上对DP概念的理解对刚学习DP算法或者利用DP解决问题的人(比如我)实际上有些困难,最好的方法是刷几道DP的题试试,得其中味之后再回头来看一下这些概念,方能茅塞顿开。