动态规划
动态规划( Dynamic Programming )是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题和最优子结构性质的问题。技术已经广泛应用于许多组合优化问题的算法设计中,比如图的多起点与多终点的最短路径问题、矩阵链的乘法问题、最大效益投资问题、背包问题、最长公共子序列问题、图像压缩问题、最大子段和问题、最优二分检索树问题、 RNA 的最优二级结构问题等。
动态规划的设计思想
动态规划法和分治法类似,它也是将大问题分解成子问题求解,不同的是,如果分解的子问题有很多是相同的,采用分治法相同的子问题会求解多次,是不是很影响效率;动态规划法呢,它会保存已解决的子问题的答案,再有相同的子问题直接用保存的答案就行了,节省了很多计算时间。
多起点、多终点的最短路径问题
例:在实际中经常遇到路径选择问题,如下图所示,有 5 个起点
S
1
,
S
2
,
.
.
.
.
.
.
.
S
5
S_1,S_2,.......S_5
S1,S2,.......S5,5个终点
T
1
,
T
2
,
.
.
.
.
.
.
T
5
T_1,T_2,......T_5
T1,T2,......T5,其余结点是途径结点,结点之间用边连接,边上的整数表示长度。从起点
S
i
S_i
Si 可以通过4条从左到右的边
S
i
—
>
A
j
—
>
B
k
—
>
C
l
—
>
T
m
S_i —> A_j—>B_k—> C_l—>T_m
Si—>Aj—>Bk—>Cl—>Tm 而到达终点
T
m
T_m
Tm ,这就是一条从起点
S
i
S_i
Si到终点
T
m
T_m
Tm 的路径,路径的长度就是这 4 条边的长度之和。我们的问题是:给定道路图,在所有起点到终点的路径中找一条长度最短的路径。
求解这个问题的蛮力算法就是穷举每一个起点到毎一个终点的所有可能的路径,然后计算每条路径的长度,从中找出最短路径。每条路径由4条边构成,除了最上边和最下边的某些结点外,处于中间的结点都有 2 条边可选,于是,从起点出发的路径大致达到
O
(
m
2
)
O ( m^2 )
O(m2)量级,其中 m 为起点个数, n 为每条路径的长度,也就是路网的层数。
下面用动态规划方法求解这个问题,从终点向起点回推,把求解过程分成 4 步,每一步对应的子问题的终点不变,但起点逐步前移,使得前步已经求解的问题恰好是后面新问题的子问题,到最后一步求解的最大的子问题正好是原始问题。具体来说,所有子问题的终点都是
T
m
(
m
=
1
,
2
,
3
,
4
,
5
)
T_m( m =1,2,3,4,5 )
Tm(m=1,2,3,4,5),但起点不同。第一步对应子问题的起点是
C
l
(
l
=
1
,
2
,
3
,
4
)
C_l ( l =1,2,3,4 )
Cl(l=1,2,3,4),第二步对应子问题的起点是
B
k
(
k
=
1
,
2
,
3
,
4
,
5
)
B_k ( k =1,2,3,4,5 )
Bk(k=1,2,3,4,5),第三步对应子问题的起点是
A
j
(
j
=
1
,
2
,
3
,
4
)
A_j ( j =1,2,3,4 )
Aj(j=1,2,3,4),第四步对应子问题的起点是
S
i
(
i
=
1
,
2
,
3
,
4
,
5
)
S_i ( i =1,2,3,4,5 )
Si(i=1,2,3,4,5),这实际上就是原始问题。在每一步需要求解的是:从当前起点到终点的最短路径及其长度。
第一步要确定从任何
C
l
C_l
Cl 到终点的最短路径。先看
C
1
C_1
C1 ,到终点只有两条路,向上走到
T
1
T_1
T1 或向下走到
T
2
T_2
T2 ,令
F
(
C
1
)
F ( C_1 )
F(C1)表示从
C
1
C_1
C1 到终点的最短路径长度,于是
F
(
C
1
)
=
m
i
n
{
C
1
T
1
,
C
1
T
2
}
=
m
i
n
{
2
,
5
}
=
2
F ( C_1 ) = min \{ C_1T_1 , C_1T_2 \} = min \{ 2,5 \} = 2
F(C1)=min{C1T1,C1T2}=min{2,5}=2
把这个结果标记在
C
1
C_1
C1 的上方,记作"
u
,
2
u , 2
u,2",其中
u
u
u 表示到终点的最短路应该“向上”。 2 表示这条最短路的长度。接着考虑
C
2
C_2
C2, 与在
C
1
C_1
C1 所做的判断类似,可以在
C
2
C_2
C2 标记为“
d
,
3
d ,3
d,3”,其中“
d
d
d ”表示到终点的最短路应该“向下”。同样的可以把
C
3
C_3
C3 和
C
4
C_4
C4 依次标记为“
u
,
7
u ,7
u,7”(或“
d
,
7
d ,7
d,7”,因为向下与向上的路径一样长)和“
u
,
1
u ,1
u,1”。到此为止第一步的判断全部完成。在这一步判断中使用的只是从
C
l
C_l
Cl 到
T
m
T_m
Tm 所有可能的边长,如果以
F
(
C
l
)
F ( C_l )
F(Cl)表示从
C
l
C_l
Cl 到终点的最短路径的长度,那么判断公式是:
F
(
C
l
)
=
m
i
n
{
C
l
T
m
}
F ( C_l ) = min\{ C_lT_m\}
F(Cl)=min{ClTm}
第二步要确定从任何
B
k
B_k
Bk 到终点的最短路径,先看
B
1
B_1
B1 ,从
B
1
B_1
B1 只能向下到
C
1
C_1
C1 ,接着从
C
1
C_1
C1 走最短路到终点,于是
F
(
B
1
)
=
B
1
C
1
+
F
(
C
1
)
=
9
+
2
=
11
F ( B_1 ) = B_1 C_1 + F ( C_1 ) = 9 + 2 = 11
F(B1)=B1C1+F(C1)=9+2=11
把这个结果标记在
B
1
B_1
B1 的上方,记作“
d
,
11
d ,11
d,11”,接着考虑
B
2
B_2
B2 ,与在
B
1
B_1
B1 所做的不同,从
B
2
B_2
B2 既可以到
C
1
C_1
C1,后接从
C
1
C_1
C1到终点的最短路;也可以先到
C
2
C_2
C2,后接
C
2
C_2
C2到终点的最短路,这两条中哪条更短,就应该选哪条。于是从
B
2
B_2
B2出发到终点的最短路长度应该是:
F
(
B
2
)
=
m
i
n
{
B
2
C
1
+
F
(
C
1
)
,
B
2
C
2
+
F
(
C
2
)
}
=
m
i
n
{
3
+
2
,
6
+
3
}
=
5
F( B_2 ) = min\{ B_2C_1+F(C_1),B_2C_2+F(C_2) \} = min\{ 3+2,6+3 \} =5
F(B2)=min{B2C1+F(C1),B2C2+F(C2)}=min{3+2,6+3}=5
这是选择先向上走的结果,因此在
B
2
B_2
B2 标记为“
u
,
5
u ,5
u,5”同样地可以把
B
3
,
B
4
,
B
5
B_3 , B_4 , B_5
B3,B4,B5,依次标记为
“
u
,
7
”
、
“
d
,
5
”
、
“
u
,
4
”
“u,7”、“d,5”、“u ,4”
“u,7”、“d,5”、“u,4”。到此为止第二步的判断全部完成。在这一步判断中使用的信息是:从
B
k
B_k
Bk到
C
l
C_l
Cl 所有可能的边长以及上一步所做的标记。递推的判断公式是:
F
(
B
k
)
=
m
i
n
{
B
k
C
l
+
F
(
C
l
)
}
F ( B_k ) = min \{ B_kC_l + F ( C_l ) \}
F(Bk)=min{BkCl+F(Cl)}
类似地可以完成后面两步的判断过程,相关的递推公式给在下面:
F
(
A
j
)
=
m
i
n
{
A
j
B
k
+
F
(
B
k
)
}
F ( A_j ) = min \{ A_jB_k + F ( B_k ) \}
F(Aj)=min{AjBk+F(Bk)}
F
(
S
i
)
=
m
i
n
{
S
i
A
j
+
F
(
A
j
)
}
F ( S_i ) = min \{ S_iA_j + F ( A_j ) \}
F(Si)=min{SiAj+F(Aj)}
各步判断所做的标记给在下图中,到全部判断完成以后,从起点到终点的最短路径已经找到了。观察在
S
i
,
S
2
,
…
,
S
5
S_i , S_2 ,…, S_5
Si,S2,…,S5 的标记,最短路的长度是10,一条从
S
3
S_3
S3 开始,追寻标记
“
d
”
“ d ”
“d”向下到
A
3
A_3
A3 ,接着按
A
3
A_3
A3处的标记
“
d
”
“ d ”
“d”向下到
B
4
B_4
B4 ,再按
B
4
B_4
B4 处的标记
“
d
”
“ d ”
“d”向下到
C
4
C_4
C4 ,最后按
C
4
C_4
C4 处的标记
“
u
”
“ u ”
“u”向上到
T
4
T_4
T4 。还有另一条是:
S
5
,
A
4
,
B
4
,
C
4
,
T
4
S_5 , A_4 , B_4 , C_4 , T_4
S5,A4,B4,C4,T4 。两条最短路径都用粗线在图中标出:
与蛮力算法相比,这种算法的好处是:在判断时只考虑由前面子问题的最优解(是当前子问题最优解的组成部分)可能的延伸结果,从而把许多不可能成为最优解的部分路径尽早从搜索中删除,因此能够提高效率,根据上面的递推公式,除终点外,对每个结点只需要做 2 次加法(对
C
i
C_i
Ci 层结点不做加法)和 1 次比较,因此算法的时间复杂度可以降到
O
(
m
n
)
O ( mn )
O(mn),其中
m
m
m 代表每层的结点个数,
n
n
n 是层数。
使用动态规划技术的必要条件
最短路径问题的动态规划算法主要特征是:
1.求解的问题是多阶段决策(优化)问题。
2.求解过程是多步判断,从小到大依次求解每个子问题,最后求解的子问题就是原始问题,子问题目标函数的最小值之间存在依赖关系,将子问题的解记录下来,以备后面求解时使用。
3.关于子问题目标函数的最小值之间的依赖关系是最关键的。
优化原则:一个最优决策序列的任何子序列本身一定是相对于子序列的初始和结束状
态最优的决策序列。
正是由于优化原则,在考虑后面较大子问题的最优解时,只需考虑它的子问题的最优解所延伸的结果,需要注意的是:在实践中并不是所有的组合优化问题都适用于优化原则,有时对某个子问题的解不一定达到最优,但是当把它延伸成整个问题的解时反而成了最优解。如果对这样的问题使用动态规划技术,就可能出错。
动态规划算法的设计要素
这里用一个矩阵链的乘法问题为例来说明动态规划算法的设计要素。
例:设
A
1
,
A
2
,
…
,
A
n
A_1, A_2 ,…, A_n
A1,A2,…,An为 n 个矩阵的序列,其中
A
i
A_i
Ai为
P
i
−
1
×
P
i
P_{i-1}×P_i
Pi−1×Pi 阶矩阵,
i
=
1
,
2
,
3...
,
n
.
i = 1,2,3...,n.
i=1,2,3...,n.这个矩阵链的输入用向量
P
=
<
P
0
,
P
1
,
…
,
P
n
>
P =< P_0 , P_1 ,…, P_n >
P=<P0,P1,…,Pn> 给出,
其中
P
0
P_0
P0 是矩阵
A
n
A_n
An 的行数,
P
i
(
i
=
1
,
2
,
…
,
n
−
1
)
P_i ( i = 1,2,… ,n -1)
Pi(i=1,2,…,n−1)既是矩阵
A
i
A_i
Ai 的列数也是矩阵
A
i
+
1
A_{i+1}
Ai+1 的行数,
最后的
P
n
P_n
Pn 是矩阵
A
n
A_n
An 的列数。
计算这个矩阵链需要做
n
−
1
n - 1
n−1 次两个矩阵的相乘运算,可以用
n
−
1
n -1
n−1对括号表示运算的顺序。
怎样定义两个矩阵相乘的工作量呢?假设矩阵
A
1
A_1
A1 与
A
2
A_2
A2 相乘,其中
A
1
A_1
A1 是
i
i
i 行
j
j
j 列的矩阵,
A
2
A_2
A2 是
j
j
j 行
k
k
k 列的矩阵,那么它们的乘积
A
1
A
2
A_1A_2
A1A2 是
i
i
i 行
k
k
k 列矩阵,含有
i
k
ik
ik 个元素,以元素相乘作为基本运算,乘积中每个元素的计算都需要做
j
j
j 次乘法,于是计算
A
1
A
2
A_1A_2
A1A2 ,总共需要
i
j
k
ijk
ijk 次乘法。
请看下面的具体实例:
假设输人的是
P
=
<
10
,
100
,
5
,
50
>
P = <10,100,5,50>
P=<10,100,5,50>,这说明有 3 个矩阵相乘,其中
A
1
:
10
×
100
,
A
2
:
100
×
5
,
A
3
:
5
×
50
A_1 : 10×100 , A_2 : 100×5, A_3 : 5×50
A1:10×100,A2:100×5,A3:5×50
有两种乘法次序,即
(
A
1
A
2
)
A
3
和
A
1
(
A
2
A
3
)
( A_1 A_2 ) A_3 和 A_1 ( A_2 A_3 )
(A1A2)A3和A1(A2A3)。如果采用第一种次序,执行的基本运算次数是:
10×100×5+10×5×50=7500
而采用第二种次序,执行的基本运算次数是:
10×100×50+100×5X50=75000
工作量相差达到10倍之多。
我们的问题是:给定向量
P
P
P,确定一种乘法次序,使得基本运算的总次数达到最少。
一般的蛮力算法就是枚举所有可能的乘法次序,针对每种次序计算基本运算的次数,从中找出具有最小运算次数的乘法次序。每一种乘法次序对应了一种在
n
n
n 个项中加
n
−
1
n -1
n−1对括号的方法。根据组合数学的知识不难知道,加
n
n
n 对括号的方法数是一个
C
a
t
a
l
a
n
Catalan
Catalan 数,它的值
1
/
n
+
1
(
n
2
n
)
1/{n+1}(^{2n}_n)
1/n+1(n2n)。根据
S
t
i
r
l
i
n
g
Stirling
Stirling 公式,即使对每种次序的计算工作量为常数,对规模为
n
+
1
n +1
n+1的输入使用蛮力算法的时间将是:
这是一个指数时间的算法。下面尝试动态规划的算法。我们将从子问题的划分、递推方程的确定、递归和迭代的实现方法、复杂度分析等万面介绍动态规划算法的设计特征。
子问题的划分和递推方程
我们的优化目标是基本运算次数的最小化,如何界定于问题的边界?
令
A
1..
n
A_{1..n}
A1..n 表示输人的矩阵链,如果从前向后划分,所产生的子问题只有后边界,是
A
1...
i
A_{1...i}
A1...i 的形式,
i
=
1
,
2
,
.
…
,
n
i = 1,2,.…,n
i=1,2,.…,n。但是在计算子问题
A
1...
j
A_{1...j}
A1...j ,
j
>
i
j > i
j>i 时,我们不仅需要子问题
A
1...
i
A_{1...i}
A1...i 的信息,也需要子问题
A
i
+
1
)
.
.
j
A_{i+1)..j}
Ai+1)..j 的信息,这说明子问题的划分需要前后两个边界,用
A
i
.
.
.
j
A_{i...j}
Ai...j 定义矩阵链
A
i
A
i
+
1
.
.
.
A
j
A_iA_{i+1}...A_j
AiAi+1...Aj,相乘的子问题,
m
[
i
,
j
]
m[i,j]
m[i,j]表示得到乘积
A
i
.
.
.
j
A_{i...j}
Ai...j 所用的最少基本运算次数。假定其最后一次相乘发生在矩阵链
A
i
.
.
.
j
A_{i...j}
Ai...j 和
A
k
+
1...
j
A_{k+1...j}
Ak+1...j 之间,即
A
i
A
i
+
1
.
.
.
A
j
=
(
A
i
A
i
+
1
.
.
.
A
k
)
(
A
k
+
1
A
k
+
2
.
.
.
A
j
)
k
=
i
,
i
+
1
,
.
.
.
,
j
−
1
A_iA_{i+1}...A_j = ( A_iA_{i+1}...A_k )( A_{k+1}A{k+2}...A_j ) k = i , i+1 ,...,j-1
AiAi+1...Aj=(AiAi+1...Ak)(Ak+1Ak+2...Aj)k=i,i+1,...,j−1
那么子问题
A
i
.
.
.
j
A_{i...j}
Ai...j 的计算依赖于子问题
A
i
.
.
.
k
,
A
k
+
1...
j
A_{i...k}, A_{k+1...j}
Ai...k,Ak+1...j 的计算j结果,换句话说,
m
[
i
,
j
]
m[ i,j ]
m[i,j] 的值依赖于
m
[
i
,
k
]
m[ i,k ]
m[i,k] 和
m
[
k
+
1
,
j
]
m[ k+1,j ]
m[k+1,j] 的值,具体的依赖关系可以表示为:
m
[
i
,
j
]
=
{
0
i = j
m
i
n
{
m
[
i
,
k
]
+
m
[
k
+
1
,
j
]
+
P
i
−
1
P
k
P
j
}
(
i
≤
k
<
j
)
i < j
m [ i , j ]= \begin{cases} 0& \text{ i = j }\\ min\{m[i,k]+m[k+1,j]+P_{i-1}P_kP_j\} (i \leq k< j)& \text{ i < j } \end{cases}
m[i,j]={0min{m[i,k]+m[k+1,j]+Pi−1PkPj}(i≤k<j) i = j i < j
上式中的 k 代表子问题的划分位置,应该考思所有可能的划分即
i
<
=
k
<
=
j
i<=k<=j
i<=k<=j ,从中比较出最小的值;
P
i
−
1
P
k
P
j
P_{i-1} P_kP_j
Pi−1PkPj ,是最后把两个子矩阵链
A
i
.
.
.
k
,
A
k
+
1...
j
A_{i...k},A_{k+1...j}
Ai...k,Ak+1...j的结果矩阵相乘所需要的基本运算次数。当
i
=
j
i=j
i=j时,矩阵链只有一个矩阵
A
i
A_i
Ai,这时乘法次数为0,对应了递推式的初值。
不难看到这个问题是满足优化原则的。这意味着当
m
[
i
,
j
]
m[i,j]
m[i,j] 达到最小值时,子问题的优化数值
m
[
i
,
k
]
m[i,k]
m[i,k] 和
m
[
k
+
1
,
j
]
m[k+1,j]
m[k+1,j] 也是最小的。因为如果对于子间题存在更好的优化函数值,比如说存在更小的
m
‘
[
i
,
k
]
m^`[ i , k ]
m‘[i,k],那么用这个更小的值替换原来的
m
[
i
,
j
]
m [ i ,j ]
m[i,j] ,就可以得到更小的
m
‘
[
i
,
j
]
m ^ ` [i,j]
m‘[i,j] ,这将与
m
[
i
,
j
]
m [ i , j ]
m[i,j]的最优性矛盾。
下面考虑算法的实现。为了确定毎一次相乘时加括号的位置,需要设计表 s [ i , j ] s [ i,j] s[i,j] 来记录 m [ i , j ] m [i , j ] m[i,j]达到最小值时 k 的划分位置,根据上面的递推公式,可以有两种实现方法:递归实现和迭代实现。
动态规划算法的递归实现
先考虑递归实现方法.
RecurMatrixChain ( P , i , j )
输人:矩阵链 Ai...j 的输人为向量 P = < Pi -1 , Pi ,…, Pj >,其中 1<= i <= j <=n
输出:计算 Ai...j 的所需最小乘法运算次数 m[ i,j ] 和最后一次运算的位置 s[ i,j ]
1. if i = j
2. then m[ i,j ] = 0 ; s[ i,j ] = i ; return m[ i,j ]
3. m[ i,j ] = ∞
4. s[ i,j ] = i
5. for k = i to j - 1 do
6. q = RecurMatrixChain( P , i , k ) + RecurMatrixChain( P , k + 1 , j ) + Pi-1 Pk Pj
7. if q < m[ i,j ]
8. then m[ i,j ] = q
9. s[ i,j ] = k
10.return m[ i,j ]
n 个矩阵相乘,只需令 i ー1, j = n ,调用算法3.1即可.下面分析这个递归算法的效率。考虑输人规模为 n 的矩阵链相乘,即=1jー n 的情况.算法在行5执行 for 循环, k 从1到ー1.每次进人循环体都在行6进行两个子问题的递归求解,其中一个规模为 k ,另一个为ーん.其余工作都是常数时间.因此该算法的时间复杂度函数满足递推关系:
T
(
n
)
≥
{
0
n = 1
∑
k
=
1
n
−
1
T
(
k
)
+
T
(
n
−
k
)
+
O
(
1
)
n > 1
T(n)\geq \begin{cases} 0& \text{ n = 1 } \\ \sum^{n-1}_ {k=1}{T(k)+T(n-k)+O(1)} & \text{ n > 1 } \end{cases}
T(n)≥{0∑k=1n−1T(k)+T(n−k)+O(1) n = 1 n > 1
经过化简得:
T
(
n
)
≥
Θ
(
n
)
+
∑
k
=
1
n
−
1
T
(
k
)
+
∑
k
=
1
n
−
1
T
(
n
−
k
)
=
Θ
(
n
)
+
2
∑
k
=
1
n
−
1
T
(
k
)
T(n) \geq \Theta(n)+\sum^{n-1}_ {k=1}{T(k)} + \sum^{n-1}_ {k=1}{T(n-k)}=\Theta(n)+2 \sum^{n-1}_ {k=1}{T(k)}
T(n)≥Θ(n)+∑k=1n−1T(k)+∑k=1n−1T(n−k)=Θ(n)+2∑k=1n−1T(k)
解得
T
(
n
)
=
Ω
(
2
n
−
1
)
T(n) = \Omega(2^{n-1})
T(n)=Ω(2n−1)
可以看到,通过使用动态规划的设计思想,该算法的时间复杂度比蛮力法有所改进。
动态规划算法的迭代实现
如果在计算中对每个子问题只计算一次,并且把结果保存起来,等到后面计算其他子问题需要这个值时,直接把它代人,这样就能够改善算法的时间复杂度。这就是迭代实现动态规划算法的思想。为了实现这个想法,需要解决以下两个问题:
(1)开辟一个存储空间,用表格的方式来存储子问题的优化函数值和划分边界,通常把这些表格叫做“备忘录”。这说明提高效率需要付出空间的代价,对于规模大的实例,过大的空间需求在往成为不能使用动态规划算法的主要因素。
(2)子问题的计算需要从底向上进行,每个子问题只计算一次。这就是说,从最小规模的子问题开始,比如规模为1的所有子问题,然后是规模为2的所有子问题,接着是规模为3的所有子问题……直到规模为 n 的子问题(原始问题)为止。这样才能在前面的计算中准备好后面计算要查找的数据。
下面的伪码给出了矩阵链问题动态规划算法的迭代实现方法:
MatrixChain( P , n )
输入:矩阵链 A1...n 的输入为向量 P = < P0,P1,...Pn >
输出:计算 Ai...j 的所需最小乘法运算次数 m[ i,j ]和最后一次运算的位置 s [ i,j ],1<=i<=j<=n
1. 令所有的 m[ i,j ] 的初值为0,s [ i,j ]初值为1,1<=i<=j<=n
2. for r = 2 to n do
3. for i = 1 to n - r + 1 do
4. j = i + r - 1
5. m[ i,j ] = m[ i+1,j ] + Pi-1 * Pk * Pj
6. s [ i,j ] = i
7. for k = i+1 to j-1 do
8. t = m [ i,k ] + m [ k+1,j ] + Pi-1 * Pk * Pj
9. if t < m [ i,j ]
10. then m [ i,j ] = t
11. s [ i,j ] = k
算法时间复杂度: W ( n ) = O ( n 3 ) W ( n ) = O( n^3 ) W(n)=O(n3) ,比起递归实现的指数时间确实有了明显的改进。
动态规划算法的典型应用
背包问题
背包问题( Knapsack Problem )是一个著名的 NP 难问题。背包问题具有广泛的应用背景。许多任务调度、资源分配、轮船装载等问题都可以用背包问题建模。
例:一个旅行者准备随身携带一个背包,可以放人背包的物品有 n 种,物品 j 的重量和价值分别为
ω
j
,
υ
j
,
j
=
1
,
2
,
.
.
.
,
n
\omega _j, \upsilon _j, j = 1,2,...,n
ωj,υj,j=1,2,...,n。如果背包的最大重量限制是
b
b
b ,怎样选择放人背包的物品以使得背包的价值最大?
这是一个组合优化问题,设
x
j
x_j
xj 表示装入背包的第
j
j
j 种物品的数量,那么目标函数和约束条件是:
目 标 函 数 : m a x ∑ j = 1 n υ j x j 目标函数:max \sum_{j=1}^n \upsilon_jx_j 目标函数:max∑j=1nυjxj
约 束 条 件 : { ∑ j = 1 n ω j x j ≤ b x j 约束条件: \begin{cases} \sum_{j=1}^n\omega_j x_j\leq b & \text{ } \\ x_j & \text{ } \end{cases} 约束条件:{∑j=1nωjxj≤bxj
如果组合优化问题的目标函数和约束条件都是线性函数,称为线性规划问题,如果线性规划问题的变量
x
j
x_j
xj 都是非负整数,则称为整数规划问题。背包问题就是整数规划问题,限制所有的
x
j
=
0
x_j=0
xj=0或
1
1
1 时的背包问题称为0-1背包问题。
背包问题有两个参数,物品种类和背包重量的限制。可以根据物品种类和背包的重量限制进行子问题划分。设
F
k
(
y
)
F_k ( y )
Fk(y) 表示只允许装前 k 种物品,背包总重不超过 y 时背包的最大价值,分两种情况考虑:
不装第 k 种物品或者至少装1件第 k 种物品,如果不装第 k 种物品,那么只能用前 k 一1种物品装人背包,而背包的重量限制仍旧是 y ,在这种情况下,背包最大价值是
F
k
−
1
(
y
)
F_{k-1}( y )
Fk−1(y)。如果第 k 种物品装了一件,那么背包的价值是
υ
k
\upsilon_k
υk,且重量是
ω
k
\omega_k
ωk ,剩下的物品仍旧需要在前 k 种物品中选择(因为最优的装法可能包含多个第 k 种物品)。于是问题归约为在背包重量限制为
y
−
ω
k
y - \omega_k
y−ωk 的情况下如何用前 k 种物品装人背包以取得最大价值
F
k
(
y
ー
ω
k
)
F_k ( y ー \omega_k )
Fk(yーωk) 。我们需要从这两种情况中取价值较大的作为最优选择。
于是得到递推关系与边界条件如下:
F
k
(
y
)
=
m
a
x
{
F
k
−
1
(
y
)
,
F
k
(
y
−
ω
k
)
+
υ
k
}
F_k( y ) = max\{F _{k-1} ( y ), F_k ( y - \omega_k ) + \upsilon_k\}
Fk(y)=max{Fk−1(y),Fk(y−ωk)+υk}
F
0
(
y
)
=
0
F_0( y ) = 0
F0(y)=0
0
≤
y
≤
b
0\leq y \leq b
0≤y≤b
F
k
(
0
)
=
0
F_k( 0 ) = 0
Fk(0)=0
0
≤
k
≤
n
0\leq k \leq n
0≤k≤n
F
1
(
y
)
=
⌊
y
/
ω
1
⌋
υ
1
F_1( y ) = \lfloor{y/\omega_1} \rfloor \upsilon_1
F1(y)=⌊y/ω1⌋υ1
F
k
(
y
)
=
−
∞
F_k( y ) = - \infty
Fk(y)=−∞
y
<
0
y<0
y<0
上面公式的初值比较多,
F
0
(
y
)
F_0( y )
F0(y)是背包不装物品的价值,显然等于
0
0
0 ;
F
k
(
0
)
F_k( 0 )
Fk(0)是背包重量限制为
0
0
0 时的最大价值,当然也等于
0
0
0 ;
F
1
(
y
)
F_1( y )
F1(y) 是只能用第一种物品背包重量限制为
y
y
y 时的最大价值,为了保证背包不超重,第一种物品至多能装
⌊
y
/
ω
1
⌋
\lfloor y / \omega_1\rfloor
⌊y/ω1⌋ 个,因此背包价值是
⌊
y
/
ω
1
⌋
υ
1
\lfloor y / \omega_1\rfloor\upsilon_1
⌊y/ω1⌋υ1 。
在递推式中有
F
k
(
y
−
ω
k
)
F_k ( y - \omega_k )
Fk(y−ωk),在递推过程中,某些
y
−
ω
k
y - \omega_k
y−ωk,可能得到负值,这意味着背包此刻能够承受的重量已经小于第
k
k
k 种物品的重量,实际上是不能装的。通过
y
<
0
y<0
y<0令
F
k
(
y
)
=
−
∞
F_k( y ) = - \infty
Fk(y)=−∞ (可以选机器中的最小数),使得这个值在与另一个优化函数值比较时被淘汰,从而使得这种装法被排除。
TrackSolution
输入:ik(y)表,其中 k=1,2,...n, y=1,1,...b
输出:x1,x2,...,xn,n种物品的装入量
1. for j = 1 to n do
2. xj = 0
3. y = b , j = n
4. j = ij(y)
5. xj = 1
6. y = y - wj
7. while ij( y ) = j do
8. y = y - wj
9. xj = xj + 1
10. if ij( y ) ≠ 0 then goto 4
考虑算法的时间复杂度,该算法的时间复杂度为 O ( n b ) O ( nb ) O(nb)。表面上看起来这是一个 n 和为变量的多项式,但这个算法并不是多项式时间的算法。因为正整数 b b b 的输人规模是 b b b 在机器中占用的存储空间大小,因此问题的输人规模实际上是 n n n 和 l o g b logb logb ( l o g b logb logb 是 b b b的二进制表示的位数),显然 O ( n b ) O ( nb ) O(nb)不是 n n n 和 l o g b logb logb 的多项式函数,我们把这样的算法称为伪多项式时间的算法。