1. 设计要素
- 问题建模,优化的目标函数是什么?约束条件是什么?
- 如何划分子问题 (边界) ?
- 问题的优化函数值与子问题的优化函数值存在 着什么依赖关系?(递推方程)
- 是否满足优化原则?
- 最小子问题怎样界定?其优化函数值,即初值等于什么?
2. 具体例子:矩阵链相乘
2.1 问题:
- 设A1,A2,… ,An 为矩阵序列 (就是一共有n个矩阵)
- Ai为Pi-1×Pi阶矩阵,i = 1,2,… ,n.
(这就确定了相邻的矩阵可以相乘) - 试确定矩阵的乘法顺序,使元素相乘的总次数最少
输入:
向量 P = <P0,P1,…,Pn>
其中P0,P1,… ,Pn为n个矩阵的行数和列数
- 例如:
P = <10,100,5,50>
A1:10×100,A2:100×5,A3:5×50
输出:
矩阵链乘法加括号的位置
2.1.1 两个矩阵元素相乘的次数
- 先算一个元素的次数:
cts = at1b1s + at2b2s + … + atjbjs
计算一个元素共 j 次乘法 - 结果共有 i 行,k 列
i 行 j 列 × j 行 k 列 = i 行 k 列
共需乘法次数:ijk
2.1.2 实例
P = <10,100,5,50>
A1:10×100,A2:100×5,A3:5×50
乘法次序
- (A1A2)A3:10 × 100 × 5 + 10 × 5 × 50 = 7500
- A1(A2A3): 100 × 5 × 50 + 10 × 100 × 50 = 75000
第一种次序计算次数最少
2.2 蛮力算法
- 如果是 m 个矩阵相乘,那么一共需要加 m - 1 对括号
- 加括号是为了确定运算顺序,如果是两个数,自然只需要确定一个运算顺序,如果是确定三个数的运算顺序,就要确定两个运算顺序,即哪两个数相乘,然后前面两个数相乘得到的结果再与最后一个数相乘,即加两个括号
- 加 n 个括号的方法有
1
n
+
1
1\over{n + 1}
n+11C2nn 种
- 如两个括号()()、(())
- 三个括号((()))、()(())、(())()、(()())、()()()
蛮力算法通过穷举所有可能的括号化方式并计算它们的成本,然后选择最小成本的方式
2.3 动态规划算法
2.3.1 算法思想
2.3.1.1 子问题划分
- 先给出一个概念
Ai…j:矩阵链AiAi+1…Aj,边界i,j - 输入向量:<Pi-1,Pi,…,Pj>
- 设其最好划分的运算次数:m[i,j]
2.3.1.2 子问题的依赖关系
最优划分最后一次相乘发生在矩阵 k 的位置
即Ai…j = Ai…k Ak+1…j
那么Ai…j 的最优运算次数依赖于Ai…k 与Ak+1…j 的最优运算次数
2.3.1.3 优化函数的递推方程
m[i,j]:得到Ai…j的最少的相乘次数
且满足优化原则(当子问题更优化时原问题也更优化)
2.3.2 递归算法
2.3.2.1 递归思想
- 对k循环,找到通过递归的方法找到一个最好的k
- 在循环的时候,如果找到一个更好的,那就替代原先的
- 这里是递归的求解m中的值的,m的值要通过RecurMatrixChain这个函数求得 (所以叫递归),(与下面的方法不同)
2.3.2.2 时间复杂度
2.3.3 迭代实现
2.3.3.1 递归算法子问题的产生
比如当 n = 5 时
- 第一次循环会产生 8 个子问题,然后每个子问题再递归产生更多子问题
- 但有些子问题会重复出现多次 (比如划分为1-1的这个子问题出现好多次,在图中看颜色区分就行)
子问题计数
- 边界不同的子问题:15个
- 递归计算的子问题总个数:81个
2.3.3.2 迭代计算关键
- 每个子问题只计算一次
- 迭代过程
- 考虑计算的顺序,以保证后面用到的值前面已经计算好
- 存储结构保存计算结果——备忘录
- 解的追踪 (我们最终的结果是要算最少的乘法次数,因此要追踪计算每次乘法的次数)
- 设计标记函数标记每步的决策
- 考虑根据标记函数追踪解的算法
2.3.3.3 矩阵链乘法子问题计算顺序
- 共有n个含1个矩阵(长度为1)的子问题
- 共有n - 1个含2个矩阵(长度为2)的子问题
- 共有n - 2个含3个矩阵(长度为3)的子问题
- …
- 共有2个含n - 1个矩阵(长度为n - 1)的子问题
- 共有1个含n个矩阵(长度为n)的问题(即原始问题)
- 假设我们就规定子问题的求解顺序为上图的顺序
2.3.3.3.1 计算顺序实例
2.3.3.4 迭代实现伪码
步骤详解
- 1 当只有1个矩阵的时候,运算次数为0次,且保证后面m值的计算都可以用前面的m的值来算,而不是递归的调用函数来求的,后面举个例子来说明
- 2-4 为了能够实现先算链长较小的子问题
- 5-6 计算此时还没划分的时候的乘法次数和k的位置,也算是给m的值先算出来一个初值,方便后面比较大小看哪种情况算出来的m的值最小
- 7-11 同上面递归的算法一样,划分k,并且找出最少的划分次数和此时对应的k
2.3.3.5 时间复杂度
根据伪码
- 行2,3,7都是O(n)
- 循环执行O(n3) 次,内部为 O(1)
- W(n) = O(n3)
2.3.3.6 实例
2.3.3.6.1 实例的备忘录m[i,j]
- r = 1,我们赋初值全部为 0
- r = 2,m[1,2] = m[2,2] + 30 × 35 × 15 = 15750 ,本来后面还有对k的划分,但是r太小了,所以这里没写,其他r = 2的值同理
- …
- r = 4,(此时 r = 3、r = 2等等这些m的值都已经算出来了) 比如 m[2,5] 的值为7125,计算过程为:
-
可以看作 m[2,2] 和m[3,5] 这两个子问题相乘
此时结果为:0 + 2500 + 35 × 15 × 20 = 13000 -
也可以看作 m[2,3] 和 m[4,5] 两个子问题
结果为 2625 + 1000 + 35 × 5 × 20 = 7125 -
或
结果为 4375 + 0 + 35 × 10 × 20 = 11375
-
因此结果为7125
2.3.3.6.2 实例的标记函数s[i,j]
- 用来记录在当前位置最佳的划分位置(即k的位置)
怎么通过这个图来找到最佳的
- 先从r = 5分析
- 由1可以看出我们把原问题转化为s[1,3]和s[4,5],于是找s[1,3]和s[4,5]
因此总的结果为