动态规划的思想:由于朴素递归算法反复求解相同的子问题,因而效率很低。动态规划仔细安排求解顺序,对每个子问题只求解一次,并将结果保存下来,如果随后再次需要子问题的解,只需要查找保存的结果,而不必重新计算。
动态规划是付出额外的内存空间来节省计算时间,是典型的时空权衡。
动态规划的步骤:
1. 刻画一个最优解的结构特征。
2. 递归的定义最优解的值。
3. 计算最优解的值,通常采用自底向下的方法。
4. 利用计算出的信息构造一个最优解。
动态规划有两种等价的实现方法:
- 带备忘的自顶向下法
- 自底向上法
最优子结构性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
如何发掘一个问题的最优子结构性质,实际上可以观察以下四条:
1. 做出一个选择,做出这种选择会产生一个或多个待解子问题。
2. 对于一个给定问题,在第一步选择中,假设已经知道了哪种选择才会得到最优解。
3. 给定可获得最优解的选择后,确定这次选择会产生哪些子问题,以及如何最好的刻画子问题空间。
4. 构成原问题最优解的某些组成部分就是子问题的最优解。
对于不同的问题,最优子结构的不同体现在:
1. 原问题的最优解涉及到多少子问题。
2. 确定最优解使用哪些子问题时,需要考虑多少种选择。
而以上两条的乘积就可以粗略估算动态规划的运行时间。
最优子结构划分的子问题必须是无关的,不能相互影响。如果求解某一子问题时用到了某些资源,那么这些资源在求解其它子问题的时候就不可用了。
重叠子问题:动态规划中包含很多重叠子问题,对于每个子问题,只求解一次,将解存入一个表中,当再次需要这个子问题时直接查表。
常见的动态规划问题:
- 钢条切割问题:
问题描述:给定一段长度为n的钢条和一个价格表P,求切割钢条方案,使得总收益rn最大。注意,如果长度为n英寸的钢条价格pn足够大,最优解可能完全不用切割。
算法设计思路:从待切割的钢条左侧切下长度为i的一段,只对右边剩下长度为n-i的一段继续切割(递归求解)。
递归关系式如下:
//对于1<=i<=n
r[n] = max(p[i] + r[n-i])
- 矩阵链乘法括号问题:
问题描述:给定n个矩阵的链(A1, A2, …An),矩阵Ai的规模为pi-1 * pi,求完全括号化方案,使得计算乘积A1A2A3…An所需标量乘法次数最少。
算法设计思路:对于Ai…Aj的优化,可以选一个分割点k,将优化问题转换为求Ai…Ak的优化加上Ak+1…Aj的优化,再加上pi-1 * pk * pj。由于k有j-i种可能的取值,因此需要从中选取最小的。
这一问题需要注意的是,求解m[i,j]时,是从对角线开始,依次往右上角求解的。
递归关系式如下:
if i == j:
m[i][j] = 0
else:
//对于i<=k<j
m[i][j] = min{m[i][k] + m[k+1][j] + p[i-1] * p[k] * p[j]}
- 最长公共子序列问题:
问题描述:给定两个序列X=(x1, x2, …, xm)和Y=(y1, y2, …, yn),求X和Y长度最长的公共子序列。
算法设计思路:找出LCS问题的最优子结构。对于X和Y两个序列,Z=(z1, z2, …, zk)为X和Y的任意LCS。
//如果xm == yn,则zk == xm == yn,且Zk-1是Xm-1和Yn-1的一个LCS
//如果xm != yn,那么zk != xm 意味着Z是Xm-1和Y的一个LCS。
//如果xm != yn,那么zk != yn 意味着Z是X和Yn-1的一个LCS。
//设c[i,j]表示Xi和Yj的LCS长度,于是可以得到递归关系式:
if i==0 or j == 0:
c[i][j] = 0
elif i,j > 0 and x[i] == y[j]:
c[i][j] = c[i-1][j-1] + 1
else:
c[i][j] = max(c[i][j-1], c[i-1][j])