动态规划(dynamic programming)通过组合子问题的解来求解原问题,“programming”指的是一种表格法。动态规划对每个子问题只求解一次,将其解存在一个表格中,从而无须每次求解一个子子问题时都重新运算。通常用来求解“最优化问题”,这类问题可以有很多可行解,每个解都有一个值,我们希望寻找具有最优值的解。
Ex1 钢条切割
第一步 :刻画最优解的结构特征
钢条的最优收益可以定义为
钢条问题满足最优子结构,即分成子问题后仍然有与原问题相同的结构,只是规模变小了
于是我们可以刻画它的最优解的结构特征
第二步:递归的定义最优解的值
#define CUT_ROD(p,n)
if n == 0
return 0
q = -10000000;
for i = 1 to n
q = max(q,CUR_ROD(p[i]+n-i))
return q
第三步:计算最优解的值,通常采用自底向上的方法
第二步通过递归实现了求解最优解,但是过程中对于一些问题进行了重复的计算,复杂度达到了指数级别
通过自底向上可以实现存储已经求解出来的值,进行直接使用
BOTTOM-UP-CUT-ROD(p,n)
let r[0..n]be a new array
r[0] = 0;
for j = 1 : n
q = -100000;
for i = 1 : j
q = max(q,p[i]+r[j-i]);
r[j]=q;
return r[n];
还可以用加入了备忘机制的递归(自顶向下方法)
MEMOIZED-CUT-ROD(p,n)
let r[0..n] be a new array
for i=0 to n
r[i]=-100000
return MEMOIZED-CUT-ROD-AUX(p,n,r)
MEMOIZED-CUT-ROD(p,n,r)
if r[n]>0
return r[n]
if n==0
q=0;
eles q = -100000;
for i = 1 to n
q = max(q,p[i]+MEMOIZED-CUT-ROD-AUX(p,n-i,r))
r[n]=q
return q
第四步:利用计算出的信息构造一个最优解
返回r[n],不同长度n的钢条的最优解。还可以用一个数组,保存长度n第一段切下来的钢条的长度。
子问题图
当思考一个动态规划问题时,应该弄清楚所涉及的子问题及子问题之间的依赖关系,比如求解这个钢条切割问题时,长度为4的钢条收益时,要先计算长度为3,长度为2,长度为1的钢条的子收益,因为长度为4的钢条收益计算时会用到,有依赖的边,自底向上的动态规划算法是按“逆拓扑序”来处理子问题图中的顶点。
什么时候应该用动态规划
问题应具有两个要素
- 最优子结构
1)如果问题的一个最优解包含了子问题的最优解,则该问题具有最优子结构。当一个问题具有最优子结构的时候,我们 就可能要用到动态规划(贪心策略也是有可能适用的)
2)寻找最优子结构时,可以遵循一种共同的模式:
a、问题的一个解可以是一个选择。例如,装配站选择问题。
b、假设对一个给定的问题,已知的是一个可以导致最优解的选择。不必关心如何确定这个选择,假定他是已知的。
c、在已知这个选择之后,要确定那些子问题会随之发生,以及如何最好的描述所的得到的子问题空间。
d、利用一种“剪贴”技术,来证明在问题的一个最优解中,使用的子问题的解本身也必须是最优的。
3)最优子结构在问题域中以两种方式变化:
a、有多少个子问题被使用在原问题的一个最优解中,以及
b、再决定一个最优解中使用那些子问题时有多少个选择
在装配线调度问题中,一个最优解只使用了一个子问题,但是,为确定一个最优解,我们必须考虑两种选择。
4)动态规划与贪心算法的区别
动态规划以自底向上的方式来利用最优子结构。也就是说,首先找到子问题的最优解,解决的子问题,然后找到问题的一个最优解。寻找问题的一个最优解需要首先在子问题中做出选择,即选择用哪一个来求解问题。问题解的代价通常是子问题的代价加上选择本身带来的开销。
在贪心算法中是以自顶向下的方式使用最优子结构。贪心算法会先做选怎,在当时看来是最优的选择,然后在求解一个结果子问题,而不是现寻找子问题的最优解,然后再做选择。
2. 子问题重叠
适用于动态规划求解的最优化问题必须具有的第二个要素是子问题的空间要“很小”,也就是用来解原问题的递归算法可以反复的解同样的子问题,而不是总在产生新的子问题。典型的,不头痛的子问题数十输入规模的一个多项式,当一个递归算法不断的调用同一问题是,我们说该最优问题包含重叠子问题。动态规划算法总是充分利用重叠子问题,即通过每个子问题只解一次,吧解保存在一个需要时就可以查看的表中,而每一次查表得时间为常数。就像钢条分割问题,长度为n的钢条进行分割,子问题只有n-1个,即从1到n-1长度的钢条的最佳收益。
题目
- https://leetcode-cn.com/problems/maximum-subarray/ 最大子序和,记录当前节点之前序列最大的和与两端的端点(子问题结构),利用这一点不断地朝前推进,然后每一步利用之前的结果计算出首节点到当前节点的最大子序和,存储在中间表格中,以供下次使用
- https://leetcode-cn.com/problems/unique-paths/ 最多路径数,又是一个求最优解的问题,因为只能向右和向下,所以子问题成为了右边点有多少条路径和下边点到终点有多少条路径,两者之和是当前点的和,自底向上,很好算,复杂度为O(n)