动态规划(dynamic programming)与分治方法相似,都是通过组合子问题的解来求解原问题。动态规划方法通常用来求解最优问题(optimization problem),这类问题可以有很多可行解,每个解都有一个值,我们希望寻找具有最优值(最小值或最大值)的解,我们称这样的解为问题的一个最优解,而不是最优解,因为可能有多个解都达到最优值。
我们通常按如下4个步骤来设计一个动态规划算法:
最前的3个步骤是动态规划算法求解问题的基础,如果我们仅仅需要一个最优解的值,而非解本身,可以忽略最后一个步骤。
我们应用动态规划来求解一个如何切割钢条的简单问题。钢条切割问题:给定一段长度为 n n n英寸的钢条和一个价格表 p i ( i = 1 , 2 , . . . , n ) p_i(i=1,2,...,n) pi(i=1,2,...,n),求切割钢条的方案,使得销售收益 r n r_n rn最大。
长度为 n n n英寸的钢条共有 2 n − 1 2^n-1 2n−1种不同的切割方案,因为在距离钢条左端 i ( i = 1 , 2 , . . . , n − 1 ) i(i=1,2,...,n-1) i(i=1,2,...,n−1)英寸处,我们总是可以选择切割或不切割。我们用普通加法符号表示切割方案,因此7=2+2+3表示将长度为7英寸的钢条切割为3段,两段长度为2英寸,一段长度为3英寸,如果一个最优解将钢条切割为 k k k段(对某个 1 ≤ k ≤ n 1 \le k \le n 1≤k≤n),那么最优切割方案 n = i 1 + i 2 + . . . + i k n = i_1+i_2+...+i_k n=i1+i2+...+ik将钢条切割为长度分别为 i 1 , i 2 , . . . , i k i_1,i_2,...,i_k i1,i2,...,ik的小段,得到最大收益 r n = p i 1 + p i 2 + . . . + p i k r_n=p_{i_1}+ p_{i_2}+...+p_{i_k} rn=pi1+pi2+...+pik
为了求解规模为 n n n的原问题,我们先求解形式完全一样,但规模更小的子问题。钢条切割问题满足最优子结构性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题k可以独立求解。
因此我们可以得到钢条切割问题的简单递归求解方法: r n = max 1 ≤ i ≤ n ( p i + r n − i ) r_n=\max_{1 \le i \le n}(p_i+r_{n-i}) rn=1≤i≤nmax(pi+rn−i)
python实现如下:
def cut_rod(p,n):
if n == 0:
return 0
#初始化效益最小值
q = -sys.maxsize
for i in range(1,n+1):
q = max(q,p[i]+cut_rod(p,n-i))
return q
cut_rod以价格数组 p [ 1.. n ] p[1..n] p[1..n]和整数 n n n为输入,返回长度为 n n n的钢条的最大收益。验证后,会发现一旦输入规模稍微变大,程序运行的时间会变得相当长。原因在于。cut_rod反复地用相同的参数值对自身进行递归调用,即它反复求解相同的子问题。如下图所示,显示了 n = 4 n=4 n=4时的调用过程,当这个过程递归展开时,它所做的工作量会爆炸性地增长。
一般来说,这棵递归调用树共有 2 n 2^n 2n个节点,其中 2 n − 1 2^{n-1} 2n−1个叶节点,所以规模为 n n n,运行时间为 T ( n ) = 2 n T(n)=2^n T(n)=2n,即cut_rod的运行时间为 n n n的指数函数
朴素递归算法之所以效率很低,是因为它反复求解相同的子问题,因此,动态规划方法对每个子问题只求解一次,并将结果保存下来,如果随后再次需要此问题的解,只需查找保存的结果,而不必重新计算。因此,动态规划方法是付出额外的内存空间来节省计算时间。
动态规划有两种等价的实现方法:
带备忘的自顶向下python实现:
def memoized_cut_rod(p,n):
#init the r[0..n]
r = [-sys.maxsize for i in range(n+1)]
return memoized_cut_rod_aux(p,n,r)
def memoized_cut_rod_aux(p,n,r):
#已经计算的子问题就直接返回
if r[n]>=0:
return r[n]
if n == 0:
q = 0
else:
q = -sys.maxsize
for i in range(1,n+1):
q = max(q, p[i]+memoized_cut_rod_aux(p, n-i, r))
#记录每次已经计算的子问题解
r[n] = q
return q
自底向上python实现:
##########自底向上####################
def bottom_up_cut_rod(p,n):
#长度为0的钢条没有收益
r = list()
# let r[0] = 0
r.append(0)
for j in range(1,n+1):
q = -sys.maxsize
#求解规模为j的子问题解
for i in range(1, j+1):
q = max(q, p[i]+r[j-i])
#将规模为j的子问题的解存入r[j]
r.append(q)
#返回最优解
return r[n]
自底向上和自顶向下算法具有相同的渐进运行时间。运行时间为 Θ ( n 2 ) \Theta(n^2) Θ(n2)
适合应用动态规划方法求解的最优化问题应该具备的两个要素:最优子结构和子问题重叠
具体代码,可参考github地址:算法导论各章节算法python实现