第五章 动态规划
- 动态规划算法与分治法类似,其基本思想也是将求解问题分解为若干个子问题。与分治法不同的是,动态规划问题中子问题不是独立的,可能重复计算,所以动态规划旨在优化问题减少重复子问题的计算。
- 简言之,动态规划的实质是分治和消除冗余。
矩阵链相乘
- 假设我们要计算三个矩阵的乘积即
,这
个矩阵的维数分别为
,我们可以有
两种方式。前者需要
次乘法,后者需要
次乘法。可见对于
个矩阵
链乘的耗费,取决于
个乘法的执行顺序。下面我们讨论找出
个矩阵链相乘的最小数乘次数。
Catalan数
-
设
是求
个矩阵相乘的所有放置括弧的方法数。对于
,对于前
个矩阵有
种放置方法,对于余下的有
种放置方法,总共有
种放置方法,则对于
个矩阵的所有放置方法数:
,显然
(两个矩阵只有一种乘法,三个矩阵有两种乘法),我们假定
,可以证明:
。
上述递推式产生了Catalan数,定义
,它的前
项为:
。因此矩阵个数
逐渐变大时,采用简单的暴力算法是十分低效的。
最小矩阵链乘数MATCHCHAIN
对于每个
,矩阵
的列数一定等于矩阵
的行数。因此我们指定
维数
,其中
分别表示矩阵
的行数和列数(最右边的矩阵的列数与前面无关,所以会有
)。并且,我们用
表示
的乘积,
的数量乘法耗费记为
。
设
,那么有
。显然这样按
划分后,
(
实际上也是两个维数分别为
的新矩阵相乘)。
于是我们得到
,对于求
,则我们需要解决递推式
。
-
为避免重复计算,考虑到
,我们将计算二维数组
的右上半部分(对角线向上计算),最终
下图中的
即为所求。
-
例:给出
个矩阵如下:(
表示第一条对角线)
易知,
上的元素均为
;
······
······依次计算
得到如下表格:
0 1 2 3 4 C[1,1]=0 C[1,2]=200 C[1,3]=320 C[1,4]=620 C[1,5]=348 C[2,2]=0 C[2,3]=240 C[2,4]=640 C[2,5]=248 C[3,3]=0 C[3,4]=240 C[3,5]=168 C[4,4]=0 C[4,5]=120 C[5,5]=0
-
MATCHCHAIN
输入:n个矩阵的链的维数对应于正整数数组r[1···n+1]。
输出:n个矩阵相乘的数量乘法最小次数。
for i <- 1 to n
C[i,i] <- 0 #对角线0的元素填充0
end for
for d <- 1 to n-1 #遍历n条对角线
for i <- 1 to n-d #遍历第d条对角线的所有元素
j <- i+d
C[i,j] <- infty
for k <- i+1 to j #尝试所有可能的k值求min
C[i,j] <- min{C[i,j], C[i,k-1]+C[k,j]+r[i]r[k]r[j+1]}
end for
end for
end for
return C[1,n]
- 观察MATCHCHAIN算法易知,三层
循环嵌套,时间复杂度为
,算法的空间复杂度取决于所需要的
数组,即
。
0/1背包问题
-
背包问题的定义如下:设
是
项物品的集合,现要将他们放入一个容量为
的背包中,对于第
个物品,
分别表示其体积和价值。
数学化描述:给出有
项元素的集合
,找出一个子集合
,使得
在约束条件
下最大。
之所以被称为0/1背包问题,是因为对于集合
的每个物品,要么被选中,要么不被选中。
-
为了装满背包我们给出以下递推式,设
表示从前
项中取出物品放入体积为
的背包的最大价值(
)。显然,
因为对于所有体积为
的背包,从
个物品中取得的最大价值为
;
因为对于体积为
的背包,前
项物品无法放入该背包。
-
:当
时,不能再容纳更多的物品,所以
就为
的价值;
-
:当
时,可以容纳第
个物品,但
要腾出
的体积去容纳第
个物品。
-
-
例:有容量为
的背包,要装入
种体积为
的物品,它们的价值分别是
,即物品
。
对于
的行和列全部填
;
,放入
物品,由于
,所以从
开始考虑,填入其价值
;
,放入
物品,由于
,所以从
开始考虑,
得到更新
;
,均可得到更新
;
,放入
物品,从
开始考虑,显然把
(5)取出放入
(4)的价值是缩小的······;直到
,
可以同时放入价值最大为
;
-
,放入
物品,从
开始考虑,
所以更新
;对于
,取出
放入
的价值更大,
······
······
最终依据表格,选择
价值最大为
;
背包KNAPSACK算法
KNAPSACK
输入:物品集合U={u1, u2,..., un},体积分别为s1, s2,..., sn,价值分别为v1, v2,..., vn,容量为C的背包。
输出:在约束条件
下最大,
。
for i <- 0 to n
V[i,0] <- 0
end for
for j <- 0 to C
V[0,j] <- 0
end for
for i <- 1 to n
for j <- 1 to C
V[i,j] <- V[i-1,j]
if si <= j then V[i,j] <- max{V[i,j], V[i-1,j-si]+vi}
end for
end for
return V[n,C]
- 由于计算表格中的每一项需要
的时间,所以KNAPSACK算法的时间复杂度为
,空间复杂度为
。
所有点对的最短路径问题
- 设
是一个有向图,问题描述为:找出每个顶点到其他所有顶点的距离,这里
到
的距离是指从
到
的最短路径长度。
- 为方便研究,设
个顶点在集合
中,顶点
的距离为
。初始时,
就是有向图的边长
,若
不直接连通则
。
Floyd算法
对于任意两个顶点
,它们可以通过其他顶点间接连通,所以我们可以通过比较
直接连通的长度与
通过顶点
间接连通的长度,选取较小值。当然,顶点
间接连通不一定只经过一个顶点
,这是一个递归过程。
-
开始时,如果
并且
是
中的边,则置
,否则置
。然后顺序迭代
次,
表示从顶点
到
的距离,且经过的中间顶点编号不大于
,则:
FLOYD
输入:以矩阵表示的有向图G。
输出:矩阵D,使得D[i,j]表示顶点i到顶点j的距离。
D <- l #将l复制到矩阵D
for k <- 1 to n
for i <- 1 to n
for j <- 1 to n
D[i,j]=min{D[i,j], D[i,k]+D[k,j]} #看是原距离更小,还是加入顶点k后更小
end for
end for
end for
return D
- FLOYD算法是三重循环嵌套,时间复杂度为
。需要二维数组
,空间复杂度为
。