参考文献:算法导论
可以使用动态规划的两个充分条件:
1.最优子结构(一个问题的最优解中包含了子问题的最优解,也可以适用贪心策略)
2.重叠子问题(一个递归树在不同的分支中可能碰到相同的子问题)
DP步骤:
1.描述最优解的结构
2.递归定义最优解的值
3.按自底向上的方式计算最优解的值
4.由计算出的结果构造一个最优解
题设:工厂有2调装配线,每条装配线都有n个装配步骤,两条装配线在执行同一步骤的时间是不同的,同一装配线由上一步骤转移到下一步骤的时间可以忽略,而有一条装配线转移到另一条需要一定的时间
我们可以令fi[j]是我们执行第i条装配线执行第j步的最短时间。
那么此时针对题设,只有两种情况,要么从装配线2转移到装配线1,要么直接从装配线1的上一步转到下一步。
如果是第1条生产线,那么我们很容易会写出一个递归式:f1[j] = min{ f1[j-1]+a1j, f2[j-1]+t2[j-1]+a1j }(t2[j-1]是从第2条装配线的第j-1步移到第一条装配线的第j步所花费的时间,a1[j]是第1条生产线执行第j步的时间)
f1[j-1]+a1j表示从装配线1的j-1步转到j步,此时花费的总时间
f2[j-1]+a1j+t2[j-1]表示从装配线2的j-1步转移到装配线1执行第j步,此时花费的总时间
如果我们使用递归计算,我们可以令调用fi[j]的次数为ri[j]
那么由递归式子,容易得出r1[j] = r2[j] = r1[j+1] + r2[j+1]
如果一条装配线由n步,那么可以得出最初的j11被调用了2^(n-1)次(我们可以建立一个树形模型来讨论递归问题,递归树在《算法导论》第一章有讲过,不是这篇文章的重点),整个算法的时间复杂度是O(2^n)这是无法忍受的低效率。
我们可以简化理解一下递归式,每次计算当前步骤的时间都会用到上一次所计算的时间,那么我们不用每次都从头开始计算,算法可以并行的解每次每条装配线的最优解,那么可以将算法优化为时间复杂度仅为O(n)
算法伪代码
FASTEST-WAY(a, t, e, x, n)
f1[1] <- e1 + a(1,1) f2[1] <- e2 + a(2,1) for j <- 2 to n do if f1[j-1] + a(1,j) <= f2[j-1]+t(2,j-1)+a(1,j) then f1[j] <- f1[j-1]+a(1,j) l1[j] <- 1 else f1[j] <- f2[j-1]+t(2,j-1)+a(1,j) l1[j] <- 2 if f2[j-1]+a(2,j) <= f1[j-1]+t(1,j-1)+a(2,j) then f2[j] <- f2[j-1] + a(2,j) l2[j] <- 2 else f2[j] <- f1[j-1]+t(1,j-1)+a(2,j) l2[j] <- 1 if f1[n]+x1 <= f2[n] + x2 then f* <- f1[n] + x1 l* <- 1 else f* <- f2[n] + x2 l* <- 2
令一个包含n个矩阵的链所有加括号的可能方案为Pn,我们可以考虑其实该链可以分裂成两个加括号的子链,其分裂位置为k,k可以在第1~n-1个矩阵之后,所以可以写出一个递归式
Pn = ∑P(k)*P(n-k) (k = 1~n-1)
那么我们用递归树的方式来分解这个加括号方案,可以理解为一个压栈和弹栈的过程,压栈次数必须大于弹栈次数,是一个Catalan数序列(Catalan数,我理解为一个古典概率问题,证明有代数和几何方式,个人感觉几何的折现法比较易懂,大家有兴趣可以看看),其解为C(2n,n)/n+1,解该递归式子的时间复杂度为O(2^n)。
我们按照DP四个步骤来分析:
MATRIX-CHAIN-ORDER(p)
n <- length[p]-1 for i <- 1 to n do m[i,i] <- 0 for l <- 2 to n do for i <- 1 to n-l+1 do j <- i+l -1 m[i,j] <- ∞ for k <- i to j-1 do q <- m[i,k]+m[k+1,j]+p(i-1)p(k)p(j) if q<m[i,j] then m[i,j] <- q s[i,j] <- k return m and s
例如:
计算下属矩阵的最小标量计算次数
A1 30*35
A2 35*15
A3 15*5
A4 5*10
A5 10*20
A6 20*25
第一步:计算1-2,2-3,3-4,4-5,5-6的计算次数
m:
1 | 2 | 3 | 4 | 5 | 6 | |
1 | 0 | 30*35*15 | ||||
2 | 0 | 35*15*5 | ||||
3 | 0 | 15*5*10 | ||||
4 | 0 | 5*10*20 | ||||
5 | 0 | 10*20*25 | ||||
6 | 0 |
s:
1 | 2 | 3 | 4 | 5 | 6 | |
1 | 0 | 1 | ||||
2 | 0 | 2 | ||||
3 | 0 | 3 | ||||
4 | 0 | 4 | ||||
5 | 0 | 5 | ||||
6 | 0 |
计算过程:
A1*A2 = m[1,1] + m[2,2] + p0p1p2 = 30*35*15 = 15750
第二步:计算1-3,2-4,3-5,4-6
m:
1 | 2 | 3 | 4 | 5 | 6 | |
1 | 0 | 15750 | 7875 | |||
2 | 0 | 2625 | 4375 | |||
3 | 0 | 750 | 2500 | |||
4 | 0 | 1000 | 3500 | |||
5 | 0 | 5000 | ||||
6 | 0 |
s:
1 | 2 | 3 | 4 | 5 | 6 | |
1 | 0 | 1 | 1 | |||
2 | 0 | 2 | 3 | |||
3 | 0 | 3 | 3 | |||
4 | 0 | 4 | 5 | |||
5 | 0 | 5 | ||||
6 | 0 |
计算过程
例如1-3:
(A1*A2)*A3 = m[1,2] + m[3,3] + p0p2p3 = 30*35*15 + 30*15*5 = 18000
A1*(A2*A3) = m[1,1] + m[2,3] + p0p1p3 = 35*15*5 + 30*35*5 = 7875
7875 < 18000
所以A1...3加括号方式为A1*(A2*A3),计算结果为7875
以此类推,最终计算结果
m:
1 | 2 | 3 | 4 | 5 | 6 | |
1 | 0 | 15750 | 7875 | 9375 | 11875 | 15125 |
2 | 0 | 2625 | 4375 | 7125 | 10500 | |
3 | 0 | 750 | 2500 | 5375 | ||
4 | 0 | 1000 | 3500 | |||
5 | 0 | 5000 | ||||
6 | 0 |
s:
1 | 2 | 3 | 4 | 5 | 6 | |
1 | 0 | 1 | 1 | 3 | 3 | 3 |
2 | 0 | 2 | 3 | 3 | 3 | |
3 | 0 | 3 | 3 | 3 | ||
4 | 0 | 4 | 5 | |||
5 | 0 | 5 | ||||
6 | 0 |
由m表可知:A1...6至少要执行15125次标量计算
由s表得出加括号的结果:
A1...6在3,4之间拆分
A1...3在1,2之间拆分
A4...6在5,6之间拆分
我们可以绘制一颗树来表示括号结构
A1...6
/ \
A1...3 A4...6
/ \ / \
A1 A2...3 A4 A5...6
/ \ / \
A2 A3 A5 A6
那么可以得出括号结构为:((A1*(A2*A3))*(A4*(A5*A6)))
由m表可知:A1...6至少要执行15125次标量计算
由s表得出加括号的结果:
A1...6在3,4之间拆分
A1...3在1,2之间拆分
A4...6在5,6之间拆分
我们可以绘制一颗树来表示括号结构
A1...6
/ \
A1...3 A4...6
/ \ / \
A1 A2...3 A4 A5...6
/ \ / \
A2 A3 A5 A6
那么可以得出括号结构为:((A1*(A2*A3))*(A4*(A5*A6)))