适合应用动态规划方法求解的最优化问题应该具备的两个要素:最优子结构和子问题重叠。
最优子结构:
在发掘最优子结构性质的过程中,实际上遵循了如下的通用模式:
1.证明问题最优解的第一个组成部分是做出一个选择。例如,选择钢条第一次切割位置,选择矩阵链的划分位置。做出这次选择会产生一个或多个待解的子问题。
2.对于一个给定的问题,在其可能的第一步选择中,你假定已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择。
3.给定可获得最优解的选择后,你确定这次选择会产生哪些子问题,以及如何最好地刻画子问题空间。
一个刻画子问题空间的好经验是:保持子问题尽可能简单,只在必要时才扩展它。
重叠子问题:子问题空间必须足够“小”,即问题的递归算法会反复地求解相同的子问题,而不是一直在生成新的子问题。如果递归算法反复求解相同的子问题,我们就称最优化问题具有重叠子问题性质。
与之相对的,适合分治方法求解的问题通常在递归的每一步都生成全新的子问题。
带备忘的递归算法:为每个子问题维护一个表项来保存它的解。每个表项的初值设置为一个特殊值,表示尚未填入子问题的解,当递归调用过程中遇到子问题时,计算其解,并存入对应表项,随后每次遇到同一个子问题,只是简单地查表,返回其解。
def memorized_matrix_chain(p):
n=len(p)-1
m=[[float("inf") for j in range(0,n+1)] for i in range(0,n+1)]
return look_up_chain(m,p,1,n)
def look_up_chain(m,p,i,j):
if m[i][j]<float("inf"):
return m[i][j]
if i==j:
m[i][j]=0
else:
for k in range(i,j):
## print("i:",i," ","j:",j," ","k:",k," ")
## print("计算","m[",i,"][",j,"]:","需要首先有:","m[",i,"][",k,"]:",m[i][k]," ","m[",(k+1),"][",j,"]:",m[k+1][j])
q=look_up_chain(m,p,i,k)+look_up_chain(m,p,k+1,j)+p[i-1]*p[k]*p[j]
## print("q:",q," ",end=' ')
if q<m[i][j]:
m[i][j]=q
## print("m[",i,"][",j,"]:",m[i][j])
## print()
print("执行","i:",i," ","j:",j)
return m[i][j]
if __name__=='__main__':
p=[30,35,15,5,10,20,25]
memorized_matrix_chain(p)
运行:
执行 i: 1 j: 1
执行 i: 2 j: 2
执行 i: 3 j: 3
执行 i: 4 j: 4
执行 i: 5 j: 5
执行 i: 6 j: 6
执行 i: 5 j: 6
执行 i: 4 j: 5
执行 i: 4 j: 6
执行 i: 3 j: 4
执行 i: 3 j: 5
执行 i: 3 j: 6
执行 i: 2 j: 3
执行 i: 2 j: 4
执行 i: 2 j: 5
执行 i: 2 j: 6
执行 i: 1 j: 2
执行 i: 1 j: 3
执行 i: 1 j: 4
执行 i: 1 j: 5
执行 i: 1 j: 6
>>> rn
15125
总之,在求解矩阵链乘法问题,我们既可以用带备忘的自顶向下动态规划的算法,也可以用自底向上的动态规划算法,时间复杂度O(n的三次方).。这两种方法都利用了重叠子问题性质,不同的子问题一共O(n的平方)个,对每个子问题,两种方法都只计算一次。而没有备忘机制的自然递归算法的运行时间为指数阶,因为它会反复求解相同的子问题。