主要是在最优解问题中。
由例子来引入:
引入
钢条切割问题
可能会想到贪心——总是尽量选价值大的,但是错误的,如下是一个反例:
考虑n = 4的情况,此时最优解是切割成两个2英寸,价值为10,而不是优先选4英寸而得到的9。
从切割入手,假设从左往右看首次切割在位置 i,将钢条分成长度为i和n-i的两段,令表示长度为i的最优子切割收益,则必有: ,这其实就是最优子结构。
现在我们知道可以将问题化为更小规模的问题,可以写出(这里相当于是切成两段后,只对其中一段继续切割),可以递归求解,时间复杂度为。
为什么复杂度那么高?分析递归树发现存在一些相同的子问题,递归函数反复地用一些相同的参数做重复的递归调用。
于是自然想到把调用过的记录下来,这其实就形成了动态规划算法。
动态规划方法仔细安排求解顺序,对每个子问题只求解一 次,并将结果保存下来。如果再次需要此子问题的解,只需查找保存的结果,而不必重新计算。
动态规划方法需要付出额外的空间保存子问题的解,是一种空间换时间的时空权衡。
我们发现,当一个问题的所有子问题最优解都求出来后,其最优解也定了,于是我们也可以自底向上地求解。比如:
通常,自顶向下法和自底向上法具有相同的渐近运行时间。
在某些特殊情况下,自顶向下法可能没有递归处理所有可能的 子问题(剪枝)。
由于自底向上法没有频繁的递归函数调用的开销,所以自底向 上法的时间复杂性函数通常具有更小的系数
子问题图
- 子问题图是一个有向图,每个顶点唯一地对应一个子问题。
- 若求子问题 x 的最优解时需要直接用到子问题 y 的最优解,则在子问题图中就会有一条从子问题x的顶点到子问题y的顶点的有向边。
- 自底向上的动态规划方法处理子问题图中顶点的顺序为: 对一个给定的子问题 x ,在求解它之前先求解邻接至它的子问题。即,对于任何子问题,仅当它依赖的所有子问题都求解完成了,才会求解它。
- 一般情况下,动态规划算法的运行时间与顶点和边 的数量至少呈线性关系。
重构解
即如何保存构成最优解的方案。
对钢条切割问题而言,可如下记录切割方案
记录切割的第一段钢条的长度s[j]
最终如此输出方案
矩阵链乘问题
给定 n 个矩阵的链,其中 i = 1,2,…,n,矩阵Ai的维数为 。求一个完全“括号化方案” ,使得计算乘积所需的标量乘法次数最小。
还是先来找最优子结构,设表示第 i 到 j 个矩阵相乘的最小代价,如果说其在 k 处划分,即这样加括号,那么如果这是 i 到 j 的最优方案, 和必然也分别是 i 到 k 和 k + 1到 j 的最优方案。
因为无论在何处划分,最终将(i 到 k 和 k+1 到 j )两个子矩阵相乘的代价是不变的(为),所以如果子问题解不是最优,那么可以用更优子问题解替换,达到整体更优,与整体已经最优矛盾。
由此我们可以写出状态转移方程:
类似地,用s[i, j]来记录每次划分的点位,以此记录方案。
时间复杂度
可以用自底向上的表格法手算:
重构解:
动态规划小结
步骤
- 第一步:证明问题满足最优性原理
所谓“问题满足最优性原理”即问题的最优决策序列具有最优子结构性。 证明问题满足最优性原理是实施动态规划的必要条件,如果证明了问题满足最优性原理,则说明用动态规划方法有可能解决该问题。
- 第二步:获得问题状态的递推关系式(即状态转移方程)
向后递推或向前递推
动态规划实质上是一种以空间换时间的技术, 它在实现的过程中,需要存储过程中产生的各种状态(中间结果),所以它的空间复杂度要大于其它的算法。
时间复杂度的分析
- 可以用子问题的总数和每个子问题需要考察多少种选择这两个因素的乘积来粗略分析动态规划算法的运行时间。
例如,n 对于钢条切割问题,共有Θ(n)个子问题,每个子问题最多需要考察n 种选择,因此运行时间为。 n 对于矩阵链乘法问题共有个子问题,每个子问题最多考察 n-1 种选择,因此运行时间为。
- 也可以用子问题图来做同样的分析。顶点对应一个子问题,需要考察的选择对应关联至子问题的边。
例如,n 钢条切割问题中n个顶点,每个顶点最多n条边; n 矩阵链乘法问题中个顶点,每个顶点最多n-1条边。
子问题无关性
能够用动态规划策略求解的问题,构成最优解的子问题间必须是无关的,即同一个原问题的一个子问题的解不影响另一个子问题的解,可以各自独立求解。
例如,最长路径问题子问题有关,最短路径问题子问题无关。
最优子结构的证明
“剪切-粘贴”:本质上是反证法,假定原问题最优解中对应的某个子问题的部分解不是该子问题的最优解,而存在“更优的子解” ,那么我们可以从原问题的解中“剪切”掉这一部分,而将“更优的子解”粘贴进去,从而得到一个比最优解“更优”的解,这与最初的解是原问题 的最优解的前提假设相矛盾。因此,不可能存在“更优的子解” 。
更多例子
最长公共子序列(LCS)
子序列:
公共子序列:
对给定的两个序列 X 和 Y ,若序列 Z 既是 X 的的子序列,也是 Y 的子序列,则称 Z 是X 和 Y 的公共子序列。
找最优子结构
设有序列,,为X和Y的一个LCS。考虑几个子问题:、、
若,则必有,
若且,则
若且,则
可见该问题具有最优子结构。
写状态转移方程
伪代码:
注意其b[i,j]对重构解的实现:
最优二叉搜索树
二叉搜索树的定义
二叉搜索树T是一棵二元树,它或者为空,或者其每个结点含有一个可以比较大小的数据元素,且有:
- T的左子树的所有元素比根结点中的元素小;
- T的右子树的所有元素比根结点中的元素大;
- T的左子树和右子树也是二叉搜索树。
给定一个n个关键字的已排序的序列K=<k1,k2,…,kn>(不失一般性,设 k1<k2<…<kn),对每个关键字ki,都有一个概率pi表示其被搜索的频率。根据ki和pi构建一个二叉搜索树T,每个ki对
应树中的一个结点。
对搜索对象x,在T中可能找到、也可能找不到:若x等于某个ki,则一定可以在T中找到结点ki,称为成功搜索。成功搜索的情况一共有n种,即x恰好等于某个ki。
若x<k1 、或 x>kn 、或 ki<x<ki+1 (1≤i<n),则在T中搜索x将失败,称为失败搜索。
为此引入外部结点d0,d1,…,dn,用来表示不在K中的值,称为伪关键字。
伪关键字在T中对应外部结点,共有n+1个,由此扩展二叉树:内结点表示关键字ki,外结点(叶子结点)表示di。这里每个di代表一个区间。
例如,d0表示所有小于k1的值, dn表示所有大于kn的值,对于i=1,…,n-1,di表示所有在ki和ki+1之间的值。
每个di也有一个概率qi,表示搜索对象x恰好落入区间di的频率。
二叉搜索树的期望搜索代价
一次搜索的代价等于从根结点开始访问结点的数量(包括外部结点)。
从根结点开始访问结点的数量等于结点在T中的深度+1; 记depthT(i)为结点i在T中的深度。
二叉搜索树T的期望代价为
最优二叉搜索树定义
对于给定的关键字及其概率集合,期望搜索代价最小的二叉搜索树称为其最优二叉搜索树。
最优子结构
最优二叉搜索树具有最优子结构:如果T是一棵相对于关键字k1,…,kn和伪关键字d0, …,dn的最优二叉搜索树,则T中一棵包含关键字ki,…,kj的子树T’必然 是相对于关键字ki,…,kj(和伪关键字di-1, …,dj)的最优二叉搜索子树。(剪切-粘贴法证明)
构建最优二叉搜索树
关键是确定根,如图,当确定为根之后,左右子树的所有节点深度+1,子树对根为的树的期望搜索代价的贡献是 其期望搜索代价 + 其所含所有结点的概率之和。
用w(i,j)表示区间节点概率之和:
用e(i,j)表示为包含关键字ki,…,kj的最优二叉搜索树的期望搜索代价
则有
其中
由此有状态转移方程
其中 j = i - 1的边界情况表示子树不包含实际的关键字,而只包含伪关键字,其期望搜索代价仅为。
重构解
定义root[i, j],保存计算e[i, j]时,使e[i, j]取得最小值的r,即 为关键字,…,的最优二叉搜索(子)树的树根。