第15章《动态规划》:动态规划求解钢条切割问题,python实现

动态规划

动态规划(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 2n1种不同的切割方案,因为在距离钢条左端 i ( i = 1 , 2 , . . . , n − 1 ) i(i=1,2,...,n-1) i(i=1,2,...,n1)英寸处,我们总是可以选择切割或不切割。我们用普通加法符号表示切割方案,因此7=2+2+3表示将长度为7英寸的钢条切割为3段,两段长度为2英寸,一段长度为3英寸,如果一个最优解将钢条切割为 k k k段(对某个 1 ≤ k ≤ n 1 \le k \le n 1kn),那么最优切割方案 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=1inmax(pi+rni)

自顶向下递归实现

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} 2n1个叶节点,所以规模为 n n n,运行时间为 T ( n ) = 2 n T(n)=2^n T(n)=2n,即cut_rod的运行时间为 n n n的指数函数

动态规划方法求解

朴素递归算法之所以效率很低,是因为它反复求解相同的子问题,因此,动态规划方法对每个子问题只求解一次,并将结果保存下来,如果随后再次需要此问题的解,只需查找保存的结果,而不必重新计算。因此,动态规划方法是付出额外的内存空间来节省计算时间。
动态规划有两种等价的实现方法:

  • 带备忘的自顶向下法(top-down with memoization):此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)当需要一个子问题的解时,过程首先检查是否已经保存过此解,如果是,则直接返回保存的值,从而节省了计算时间。
  • 自底向上法(bottom-up method):恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”子问题的求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已保存。

带备忘的自顶向下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实现

猜你喜欢

转载自blog.csdn.net/BGoodHabit/article/details/105964828