文章目录
动态规划原理
动态规划(Dynamic Programming)的与分治方法相似,都是通过组合子问题的解来求解原问题,其中的“Programming”并非“编程”之意,而是“借助表格求解问题”的意思。
- 分治算法的思想是将计算问题分解为规模较小的相似的子问题,然后分别求解这些子问题,再将子问题的解合并为原始问题的解。分治算法相对简单、直观、独立地处理各个子问题,而不对划分产生的子问题的特性和相互联系进行研究,导致了求解某些子问题的分治算法效率不高。
- 与之相反,动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)。在这种情况下,分治算法会反复地求解公共子问题;而动态规划算法对每个子问题只求解一次,并将其解保存在一个表格中,从而无需每次对一个子问题都重复计算,避免了大量不必要的计算工作。
动态规划方法通常用来求解最优化问题(optimization problem),最优化问题指在一组给定的约束条件 C C C 和一个实值代价函数 F ( x ) F(x) F(x) 下求解满足 C C C 并使得 F ( x ) F(x) F(x) 达到最小值或最大值的一个结构 x x x。
动态规划采用分治的思想求解计算问题,并利用子问题之间的关联特性来提高计算效率,其计算过程依赖两个特征:
-
优化子结构(最优子结构):如果最优化问题的最优解可以通过它的一系列子问题的最优解构造得到,则称该最优化问题具有优化子结构。
也可以说如果一个问题的最优解包含其子问题的最优解,我们就称此问题具有优化子结构。
有优化子结构性质,我们就可以利用这种子结构从子问题的最优解构造出原问题的最优解。
作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解,这一点可以通过反证法来证明:假定子问题的解不是其自身的最优解,那么我们就可以从原问题的解中“剪切”掉这些非最优解,将最优解“粘贴”进去,从而得到原问题的一个更优的解,这与最初的解是原问题的前提假设矛盾。当然具有优化子结构性质可能意味着适合应用贪心策略。
-
重叠子问题:如果根据优化问题的优化子结构直接采用分治方法求解该问题将导致某些子问题重复计算,则称该优化问题具有重叠子问题。
即问题的递归算法会反复地求解相同的子问题,而不是一直生成新的子问题,我们就称最优化问题具有重叠子问题。
动态规划算法通常利用重叠子问题性质,对每个子问题求解一次,将解存入一个表中,当再次需要这个子问题的时候直接查表,每次查表的代价为常数时间。
对于具有优化子结构和重叠子问题的优化问题,可以根据优化子结构来设计数据结构和子问题的计算顺序,从规模较小的子问题开始自底向上地计算各个子问题的解,确保每个子问题仅求解一次,将求得的子问题的解和构造最优价所需要的信息存储在数据结构中。最后再根据构造最优解的信息得到优化问题的解。由此可见,针对优化问题设计动态规划算法大致分为以下四个步骤:
- 分析优化子结构和重叠子问题。
该步骤分析优化问题是否有优化子结构和重叠子问题。寻找问题的优化子结构需要通过研究问题和不断尝试,并要求算法设计者具有一定的想象力和创新思维。尽管如此,优化子结构仍表现出一些常用的模式,如后缀形式(如最长公共子序列问题),中缀形式(矩阵链乘问题),前缀形式(0-1背包问题)和子树子问题(如最优二叉搜索树问题)。 - 递归地定义最优解的值。
该步骤递归地定义优化子结构中各个子问题的解,并根据优化子结构将规模较大的子问题的解(或解的代价)通过恰当的数学运算表达成规模较小的子问题的解(或解的代价)。通常,递归方程的初始值给出了规模最小的子问题的解(或解的代价)。 - 自底向上地计算最优解的值。
根据第二步得到的递归方程及其初始化条件,设计数据结构和子问题的计算顺序,确保处理规模较大的子问题时递归方程中涉及的规模较小的子问题的解均已被计算出来并存储在数据结构中,这使得相应子问题的解可以通过查询数据结构来获得。然后,根据递归方程的初始条件,自底向上地计算最优解的代价并保存,获取构造最优解的信息。 - 利用计算出的信息构造最优解。
根据第三步获得的构造最优解的信息,最终将问题的解构造出来。前三步是动态规划算法求解问题的基础,如果我们仅仅需要一个最优解的值而非解本身,可以忽略该步骤;否则就需要在执行步骤 3 时维护一些额外信息以便用来构造一个最优解。
动态规划方法是付出额外内存空间来节省计算时间,是典型的时空权衡(time-memory trade-off)的例子。而时间上的节省可能是非常巨大的:可能将一个指数时间的朴素递归算法转化为一个多项式时间的解法。如果子问题的数量是输入规模的多项式函数,而我们可以在多项式时间内由子子问题的最优解构造出每个子问题的最优解,那么动态规划方法的总运行时间就是多项式阶的。
最长公共子序列
定义
给定序列X,Y,Z,如果Z既是X的子序列又是Y的子序列,则称Z是X和Y的公共子序列。X,Y的长度最长的公共子序列成为X,Y的最长公共子序列,记为LCS(X, Y);
子序列与子串不相同,子序列中的元素可以不连续。如Z=<B,C,D,B>
是X=<A,B,C,B,D,A,B>
的子序列。
现假设序列 X = x 1 , x 2 , … , x m X=x_1,x_2,…,x_m X=x1,x2,…,xm, Y = y 1 , y 2 , … , y n Y = y_1,y_2,…,y_n Y=y1,y2,…,yn
分析优化子结构
分析:
- 如果 x m = y n x_m = y_n xm=yn,则 LCS(X, Y) 的最末字符应为公共字符 < x m = y n > <x_m = y_n> <xm=yn>,而其余部分应为前缀 X m − 1 X_{m-1} Xm−1和 Y n − 1 Y_{n-1} Yn−1的最长公共子序列,即需要求解子问题 LCS( X m − 1 , Y n − 1 X_{m-1}, Y_{n-1} Xm−1,Yn−1).
- 如果 x m ≠ y n x_m ≠ y_n xm=yn,则LCS(X, Y)的最末字符不可能既为 x m x_m xm,又为 y n y_n yn。若最末字符不为 y n y_n yn,则LCS(X, Y)与字符 y n y_n yn无关,即只需求解子问题LCS( X , Y n − 1 X, Y_{n-1} X,Yn−1).
- 若最末字符不为 x m x_m xm,则LCS(X, Y)与字符 x m x_m xm无关,即只需求解子问题LCS( X m − 1 , Y n X_{m-1}, Y_{n} Xm−1,Yn).
上述直观分析结果表明,无论何种情况,LCS(X, Y)均可以由某些子问题的解构造得到,即最长公共子序列具有优化子结构。
L
C
S
(
X
,
Y
)
=
{
L
C
S
(
X
m
−
1
,
Y
n
−
1
)
+
<
x
m
=
y
n
>
如
果
x
m
=
y
n
L
C
S
(
X
m
−
1
,
Y
)
如
果
x
m
≠
y
n
,
z
k
≠
x
m
L
C
S
(
X
,
Y
n
−
1
)
如
果
x
m
≠
y
n
,
z
k
≠
y
n
LCS(X, Y)=\left\{ \begin{array}{ll} LCS(X_{m-1}, Y_{n-1}) + < x_m = y_n> & 如果x_m = y_n\\ LCS(X_{m-1}, Y) & 如果x_m ≠ y_n,z_k ≠x_m\\ LCS(X, Y_{n-1}) & 如果x_m ≠ y_n,z_k ≠y_n \end{array} \right.
LCS(X,Y)=⎩⎨⎧LCS(Xm−1,Yn−1)+<xm=yn>LCS(Xm−1,Y)LCS(X,Yn−1)如果xm=yn如果xm=yn,zk=xm如果xm=yn,zk=yn
分析重叠子问题
L C S ( X , Y ) LCS(X, Y) LCS(X,Y) =====> L C S ( X m − 1 , Y ) LCS(X_{m-1}, Y) LCS(Xm−1,Y), L C S ( X , Y n − 1 ) LCS(X, Y_{n-1}) LCS(X,Yn−1), L C S ( X m − 1 , Y n − 1 ) LCS(X_{m-1}, Y_{n-1}) LCS(Xm−1,Yn−1)
L C S ( X m − 1 , Y ) LCS(X_{m-1}, Y) LCS(Xm−1,Y) =====> L C S ( X m − 2 , Y ) LCS(X_{m-2}, Y) LCS(Xm−2,Y), L C S ( X m − 1 , Y n − 1 ) LCS(X_{m-1}, Y_{n-1}) LCS(Xm−1,Yn−1), L C S ( X m − 2 , Y n − 1 ) LCS(X_{m-2}, Y_{n-1}) LCS(Xm−2,Yn−1)
L C S ( X , Y n − 1 ) LCS(X, Y_{n-1}) LCS(X,Yn−1) =====> L C S ( X m − 1 , Y n − 1 ) LCS(X_{m-1}, Y_{n-1}) LCS(Xm−1,Yn−1), L C S ( X , Y n − 2 ) LCS(X, Y_{n-2}) LCS(X,Yn−2), L C S ( X m − 1 , Y n − 2 ) LCS(X_{m-1}, Y_{n-2}) LCS(Xm−1,Yn−2)
L C S ( X m − 1 , Y n − 1 ) LCS(X_{m-1}, Y_{n-1}) LCS(Xm−1,Yn−1) =====> L C S ( X m − 2 , Y n − 1 ) LCS(X_{m-2}, Y_{n-1}) LCS(Xm−2,Yn−1), L C S ( X m − 1 , Y n − 2 ) LCS(X_{m-1}, Y_{n-2}) LCS(Xm−1,Yn−2), L C S ( X m − 2 , Y n − 2 ) LCS(X_{m-2}, Y_{n-2}) LCS(Xm−2,Yn−2)
子问题 L C S ( X m − 2 , Y n − 1 ) LCS(X_{m-2}, Y_{n-1}) LCS(Xm−2,Yn−1)和 L C S ( X m − 1 , Y n − 2 ) LCS(X_{m-1}, Y_{n-2}) LCS(Xm−1,Yn−2)均被重复计算。这说明最长公共子序列问题具有重叠子问题。
递归定义最优解的代价
令C[i, j]
表示
X
i
,
Y
j
X_{i}, Y_{j}
Xi,Yj的最长公共子序列的长度。
C
[
i
,
j
]
=
{
0
如
果
i
=
0
或
j
=
0
C
[
i
−
1
,
j
−
1
]
+
1
如
果
0
<
i
≤
m
,
0
<
j
≤
n
且
x
i
=
y
j
m
a
x
(
C
[
i
,
j
−
1
]
,
C
[
i
−
1
,
j
]
)
如
果
0
<
i
≤
m
,
0
<
j
≤
n
且
x
i
≠
y
j
C[i, j]=\left\{ \begin{array}{ll} 0 & 如果\ i=0\ 或\ j=0\\ C[i-1, j-1] + 1 & 如果\ 0<i\leq m,0<j\leq n \ 且\ x_i=y_j\\ max(C[i, j-1], C[i-1, j]) & 如果\ 0<i\leq m,0<j\leq n \ 且\ x_i≠y_j \end{array} \right.
C[i,j]=⎩⎨⎧0C[i−1,j−1]+1max(C[i,j−1],C[i−1,j])如果 i=0 或 j=0如果 0<i≤m,0<j≤n 且 xi=yj如果 0<i≤m,0<j≤n 且 xi=yj
自底向上计算解的代价
根据最优解的代价方程,可以采用二维数组C[0:m][0:n]
存储所有子问题的解,C[i,j]
记录
L
C
S
(
X
i
,
Y
j
)
LCS(X_i, Y_j)
LCS(Xi,Yj)的长度。
根据初始化条件,C[0,j] = C[i,0] = 0
。为了确保计算C[i,j]
之前,相关的三个子问题的解的代价C[i-1, j-1], C[i, j-1], C[i-1, j]
均已经被计算出来。这样可以采用逐行计算、逐列计算等顺序进行计算。
另外用二维数组B[1:m][1:n]
记录构造最优解的信息。
B[i,j] = '↖'
表示C[i,j] = C[i-1, j-1] + 1
B[i,j] = '←'
表示C[i,j] = C[i, j-1]
B[i,j] = '↑'
表示C[i,j] = C[i-1, j]
1. m <—— length(X), n <—— length(Y);
2. For i from 0 to m: Do C[i,0] = 0;
3. For j from 0 to n: Do C[0,j] = 0;
4. For i from 0 to m:
5. for j from 0 to n:
6. if xi = yj: Then C[i,j] = C[i-1, j-1] + 1, B[i,j] = ↖;
7. else if C[i-1,j] >= C[,j-1]: Then C[i,j] = C[i-1,j], B[i,j] = ↑;
8. else C[i,j] = C[i,j-1], B[i,j] = ←;
9. 输出 C and B
构造最优解
构造过程从B[m,n]
出发,根据B
中记录的“指针”来访问相应的数据。
如果B[i,j] = ↖
,则表明
X
i
=
Y
j
X_i = Y_j
Xi=Yj是最长公共子序列的末尾字符。
矩阵链乘法
定义
输入:n个矩阵 A 1 , … A_1,… A1,…A_n,其规模存储于数组 P [ 0 : n ] P[0:n] P[0:n], A i A_i Ai是 P [ i − 1 ] ∗ P [ i ] P[i-1] * P[i] P[i−1]∗P[i]的矩阵。
输出:计算连乘积 A 1 ⋅ A 2 ⋅ … ⋅ A n − 1 ⋅ A n A_1·A_2·…·A_{n-1}·A_{n} A1⋅A2⋅…⋅An−1⋅An的代价最小的乘法方案。
分析优化子结构
任何一个乘法方案 F F F 必然在某个 k k k 值( 1 ≤ k ≤ n − 1 1 \leq k \leq n-1 1≤k≤n−1)上按照 ( A 1 ⋅ A 2 ⋅ … ⋅ A k ) ⋅ ( A k + 1 ⋅ A k + 2 ⋅ … ⋅ A n ) (A_1·A_2·…·A_k)\ ·(A_{k+1}·A_{k+2}·…·A_{n}) (A1⋅A2⋅…⋅Ak) ⋅(Ak+1⋅Ak+2⋅…⋅An)计算连乘积。如果 F F F是代价最小的乘法方案,则 F 1 , k F_{1,k} F1,k必是 A 1 ⋅ A 2 ⋅ … ⋅ A k A_1·A_2·…·A_k A1⋅A2⋅…⋅Ak的代价最小的方案,而 F k + 1 , n F_{k+1,n} Fk+1,n必是 A k + 1 ⋅ A k + 2 ⋅ … ⋅ A n A_{k+1}·A_{k+2}·…·A_{n} Ak+1⋅Ak+2⋅…⋅An的代价最小的乘法方案;否则,将 F 1 , k F_{1,k} F1,k或 F k + 1 , n F_{k+1,n} Fk+1,n调换成连乘积代价更小的方案,则将得到带价比F更小的方案。这说明,问题的解可以通过子问题的解构造得到,即问题具有优化子结构。
分析重叠子问题
以 A 1 ⋅ A 2 ⋅ A 3 ⋅ A 4 A_1·A_2·A_3·A_4 A1⋅A2⋅A3⋅A4举例,子问题 A 1 ⋅ A 2 , A 2 ⋅ A 3 , A 3 ⋅ A 4 A_1·A_2,A_2·A_3,A_3·A_4 A1⋅A2,A2⋅A3,A3⋅A4均被重复计算。这说明最长公共子序列问题具有子问题重叠性。
递归定义最优解的代价
求解矩阵链乘法的最优方案时需要处理的子问题均是计算连续若干个矩阵链乘的最优方案,即求解形如 A i ⋅ A i + 1 ⋅ … ⋅ A j A_i·A_{i+1}·…·A_{j} Ai⋅Ai+1⋅…⋅Aj的连乘积的最优乘法方案。由于 i ≤ j i\leq j i≤j,故求解矩阵链乘法时需要考虑的子问题共有 n ( n − 1 ) / 2 n(n-1) / 2 n(n−1)/2个。于是子问题空间的大小为 O ( n 2 ) O(n^2) O(n2)。
令
m
i
j
m_{ij}
mij表示连乘积
A
i
⋅
A
i
+
1
⋅
…
⋅
A
j
A_i·A_{i+1}·…·A_{j}
Ai⋅Ai+1⋅…⋅Aj的最优乘法方案的代价。由矩阵链乘法的优化子结构及上面分析可以得到
m
i
j
=
0
如
果
i
=
j
m
i
j
=
m
i
n
i
≤
k
<
j
(
m
i
k
+
m
k
+
1
j
+
P
i
−
1
P
k
P
j
)
如
果
i
<
j
\begin{array}{ll} m_{ij} = 0 & 如果\ i = j\\ m_{ij} = min_{i \leq k < j}(m_{ik} + m_{k+1j} + P_{i-1}P_{k}P_{j}) & 如果\ i < j \end{array}
mij=0mij=mini≤k<j(mik+mk+1j+Pi−1PkPj)如果 i=j如果 i<j
自底向上计算解的代价
根据最优解的代价方程,可以采用二维数组 M [ 1 : n ] [ 1 : n ] M[1:n][1:n] M[1:n][1:n]存储所有子问题的解的代价, M [ i , j ] M[i,j] M[i,j]记录 A i ⋅ A i + 1 ⋅ … ⋅ A j A_i·A_{i+1}·…·A_{j} Ai⋅Ai+1⋅…⋅Aj的最优乘法方案的代价。
根据初始化条件, M [ i , i ] = 0 M[i,i]=0 M[i,i]=0,其中 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n。为了避免子问题重复求解,在计算 M [ i , j ] M[i,j] M[i,j]之前,需要确保相关的子问题已被计算出来,这需要按照子问题规模递增的顺序进行处理,即先处理1个矩阵的连乘积,再处理两个矩阵的连乘积,再处理三个矩阵的连乘积……于是,我们从主对角线开始,依次处理每条对角线元素即可,每条对角线逐行处理各个元素。
另用二位数组
S
[
1
:
n
]
[
1
:
n
]
S[1:n][1:n]
S[1:n][1:n]记录构造最优解的信息。由于使得方程
m
i
j
=
m
i
n
i
≤
k
<
j
(
m
i
k
+
m
k
+
1
j
+
P
i
−
1
P
k
P
j
)
m_{ij} = min_{i \leq k < j}(m_{ik} + m_{k+1j} + P_{i-1}P_{k}P_{j})
mij=mini≤k<j(mik+mk+1j+Pi−1PkPj)
取等号的
k
k
k值意味着连乘积
A
i
⋅
A
i
+
1
⋅
…
⋅
A
j
A_i·A_{i+1}·…·A_{j}
Ai⋅Ai+1⋅…⋅Aj应按照
(
A
i
⋅
…
⋅
A
k
)
⋅
(
A
k
+
1
⋅
A
k
+
1
⋅
…
⋅
A
j
)
(A_i·…·A_k)·(A_{k+1}·A_{k+1}·…·A_{j})
(Ai⋅…⋅Ak)⋅(Ak+1⋅Ak+1⋅…⋅Aj)
进行计算,故
S
[
i
]
[
j
]
S[i][j]
S[i][j]记录该k值即可。
输入:矩阵A1,……,An的规模,存储于数组P[0:n]中,Ai是P[i-1] * P[i]矩阵
输出:矩阵链乘法相关子问题的最优代价矩阵M[][]和构造最优解信息的矩阵S[][]
1. n = length(P[]) - 1; // 矩阵个数
2. for(i=0; i<n; i++) Do M[i,i] = 0; // 一个矩阵连乘积的情况
3. for(l=2; l<=n; l++) Do // 处理l个矩阵连乘积的情况
4. for(i=0; i<n-l+1; i++) Do // 沿着对角线依次处理每行的元素
5. j = i + l - 1;
6. M[i, j] = ∞;
7. for(k=i; k<j-1; k++) Do
8. q = M[i,k] + M[k+1,j] + P[i-1]P[k]P[j];
9. if q < M[i, j] Then M[i,j] = q; S[i,j] = k;
时间复杂度为 O ( n 3 ) O(n^3) O(n3),空间复杂度为 O ( n 2 ) O(n^2) O(n2).
构造最优解
由于 S [ 1 ] [ n ] = k S[1][n] = k S[1][n]=k表明连乘积 A 1 ⋅ A 2 ⋅ … ⋅ A n A_1·A_2·…·A_n A1⋅A2⋅…⋅An的最优乘法方案应按照 ( A 1 ⋅ A 2 ⋅ … A k ) ⋅ ( A k + 1 ⋅ A k + 2 ⋅ … ⋅ A n ) (A_1·A_2·…A_k)\ ·(A_{k+1}·A_{k+2}·…·A_{n}) (A1⋅A2⋅…Ak) ⋅(Ak+1⋅Ak+2⋅…⋅An)进行,所以只需要递归地构造 ( A 1 ⋅ A 2 ⋅ … A k ) (A_1·A_2·…A_k) (A1⋅A2⋅…Ak)以及 ( A k + 1 ⋅ A k + 2 ⋅ … ⋅ A n ) (A_{k+1}·A_{k+2}·…·A_{n}) (Ak+1⋅Ak+2⋅…⋅An)的乘法方案,并相应地进行合并即可。
0-1背包问题
定义
输入:物品重量 w 1 , w 2 , … , w n w_1,w_2,…,w_n w1,w2,…,wn及其价值 v 1 . v 2 , … , v n v_1.v_2,…,v_n v1.v2,…,vn,背包容量 C C C,其中 w i , v i , C > 0 w_i,v_i,C > 0 wi,vi,C>0.
输出:向量 < x 1 , x 2 , … , x n > <x_1,x_2,…,x_n> <x1,x2,…,xn>,其中 x i ∈ { 0 , 1 } x_i∈\left\{0, 1\right\} xi∈{0,1},使得 ∑ i = 1 n x i ⋅ w i ≤ C \sum_{i=1}^nx_i·w_i \leq C ∑i=1nxi⋅wi≤C且 ∑ i = 1 n x i ⋅ v i \sum_{i=1}^nx_i·v_i ∑i=1nxi⋅vi达到最大值。
分析优化子结构
如下简单的策略可以将0-1背包问题的求解过程转换成对子问题的处理。考察对第 1 个物品的处理,至多存在两种策略。策略一,将它不放入背包,即 x 1 = 0 x_1=0 x1=0,此时仅需要将第 2 ∼ n 2\sim n 2∼n个物品放入容量为 C C C的背包中,即求解子问题 < w 2 , … , w n ; v 2 , … , v n ; C > <w_2,…,w_n;v_2,…,v_n;C> <w2,…,wn;v2,…,vn;C>;策略2,将它放入背包,即 x 1 = 1 x_1 = 1 x1=1,此时仅需要将第 2 ∼ n 2 \sim n 2∼n个物品放入容量为 C − w 1 C - w_1 C−w1的背包中,确保不超重且物品价值达到最大,即求解子问题 < w 2 , … , w n ; v 2 , … , v n ; C − w 1 > <w_2,…,w_n;v_2,…,v_n;C-w_1> <w2,…,wn;v2,…,vn;C−w1>。
如果 < x 1 , x 2 , … , x n > <x_1,x_2,…,x_n> <x1,x2,…,xn>是0-1背包问题 < w 1 , w 2 , … , w n ; v 1 , v 2 , … , v n ; C > <w_1,w_2,…,w_n;v_1,v_2,…,v_n;C> <w1,w2,…,wn;v1,v2,…,vn;C>的最优解,则 < x 2 , x 3 , … , x n > <x_2,x_3,…,x_n> <x2,x3,…,xn>是 < w 2 , … , w n ; v 2 , … , v n ; C − x 1 ⋅ w 1 > <w_2,…,w_n;v_2,…,v_n;C-x_1·w_1> <w2,…,wn;v2,…,vn;C−x1⋅w1>的最优解。
0-1背包问题具有优化子结构。问题的优化子结构意味着,可以先求解子问题 < w 2 , … , w n ; v 2 , … , v n ; C − x 1 ⋅ w 1 > <w_2,…,w_n;v_2,…,v_n;C-x_1·w_1> <w2,…,wn;v2,…,vn;C−x1⋅w1>的最优解,然后处理第一个物品,根据是丢弃第一个物品还是将它放入背包会使整体方案中背包内物品总价值达到最大,决定 x 1 = 0 x_1=0 x1=0或 x 1 = 1 x_1=1 x1=1。
分析重叠子问题
由于背包容量是连续型变量,不易分析,但直观想,是存在重叠子问题的。
递归定义最优解的代价
重复上面产生子问题的过程,可以看到需要处理的子问题的一般形式是将第
i
∼
n
i \sim n
i∼n个物品放入容量为
j
j
j的背包中,即处理子问题
<
w
i
,
w
i
+
1
,
…
,
w
n
;
v
i
,
v
i
+
1
,
v
n
;
j
>
<w_i,w_{i+1},…,w_n;v_i,v_{i+1},v_n;j>
<wi,wi+1,…,wn;vi,vi+1,vn;j>。
由于
i
≤
n
,
j
≤
C
i \leq n, j \leq C
i≤n,j≤C,故0-1背包问题需要处理的子问题共有
n
C
nC
nC个。于是子问题空间的大小为
O
(
n
C
)
O(nC)
O(nC).
令
b
i
,
j
b_{i,j}
bi,j表示子问题
<
w
i
,
w
i
+
1
,
…
,
w
n
;
v
i
,
v
i
+
1
,
v
n
;
j
>
<w_i,w_{i+1},…,w_n;v_i,v_{i+1},v_n;j>
<wi,wi+1,…,wn;vi,vi+1,vn;j>的最优解的代价,即将第
i
∼
n
i \sim n
i∼n个物品放入容量为
j
j
j的背包中取得的最大价值。将优化子结构应用于该子问题,需要处理的一个子问题为
<
w
i
+
1
,
…
,
w
n
;
v
i
+
1
,
…
,
v
n
;
j
>
<w_{i+1},…,w_n;v_{i+1},…,v_n; j>
<wi+1,…,wn;vi+1,…,vn;j>,其代价为
b
i
+
1
,
j
b_{i+1,j}
bi+1,j,需要处理的另一个子问题为
<
w
i
+
1
,
…
,
w
n
;
v
i
+
1
,
…
,
v
n
;
j
−
w
i
>
<w_{i+1},…,w_n;v_{i+1},…,v_n;j-w_i>
<wi+1,…,wn;vi+1,…,vn;j−wi>(此时要求
j
≥
w
i
j \geq w_i
j≥wi),其代价为
b
i
+
1
,
j
−
w
i
b_{i+1,j-w_{i}}
bi+1,j−wi。由此,容易建立
b
i
,
j
b_{i,j}
bi,j的如下递归式
b
n
,
j
=
0
如
果
j
<
w
n
b
n
,
j
=
v
n
如
果
j
≥
w
n
b
i
,
j
=
b
i
+
1
,
j
如
果
j
<
w
i
b
i
,
j
=
m
a
x
{
b
i
+
1
,
j
,
v
i
+
b
i
+
1
,
j
−
w
i
}
如
果
j
≥
w
i
\begin{array}{ll} b_{n,j} = 0 & 如果\ j < w_n \\ b_{n,j} = v_n & 如果\ j \geq w_n \\ b_{i,j} = b_{i+1,j} & 如果\ j < w_i \\ b_{i,j} = max\left\{b_{i+1,j},v_i + b_{i+1,j-w_i}\right\} & 如果\ j\geq w_i \end{array}
bn,j=0bn,j=vnbi,j=bi+1,jbi,j=max{bi+1,j,vi+bi+1,j−wi}如果 j<wn如果 j≥wn如果 j<wi如果 j≥wi
自底向上计算解的代价
根据最优解的代价方程,可以采用二维数组 B [ 1 : n ] [ 0 : C ] B[1:n][0:C] B[1:n][0:C]存储所有子问题的代价, B [ i , j ] B[i,j] B[i,j]记录子问题 < w i , … , w n ; v i , … , v n ; j > <w_i,…,w_n;v_i,…,v_n;j> <wi,…,wn;vi,…,vn;j>的最优解的代价(这里指的是背包内物品最大价值)。
根据初始化条件,也就是仅考虑最后一件物品,有如下
- B [ n , j ] = 0 B[n,j]=0 B[n,j]=0,其中 0 ≤ j < w n 0 \leq j < w_n 0≤j<wn
- B [ n , j ] = v n B[n,j]=v_n B[n,j]=vn,其中 w n ≤ j ≤ C w_n \leq j \leq C wn≤j≤C
为了避免子问题重复求解,在计算 B [ i , j ] B[i,j] B[i,j]之前,需要确保相关的子问题 B [ i + 1 , j ] B[i+1,j] B[i+1,j]以及 B [ i + 1 , j − w i ] B[i+1,j-w_i] B[i+1,j−wi]已经被计算出来,这些问题恰好位于 B [ i , j ] B[i,j] B[i,j]的下一行。于是根据初始化条件,我们只需从第 n n n行开始,依次处理 n − 1 , n − 2 , … , 2 , 1 n-1,n-2,…,2,1 n−1,n−2,…,2,1行,对每行的处理,只需要依次处理各列的子问题。
值得注意的是,这恰好是按照子问题规模递增的顺序进行处理,即先处理含1个物品的子问题,再处理含2个物品的子问题,再处理含3个物品的子问题,等等。
输入:正整数重量数组W[1:n]和正整数价值量数组V[1:n],正整数容量C
输出:0-1背包问题所有相关问题的最优解代价数组B[1:n][0:C]
1. n = W.length();
2. For j from 0 to W[n] - 1 Do
3. B[n][j] = 0;
4. For j from W[n] to C Do
5. B[n][j] = V[n];
6. For i from n-1 to 2 Do
7. For j from o to W[i] - 1 Do
8. B[i][j] = B[i+1][j];
9. For j from W[i] to C Do
10. B[i][j] = max(B[i+1][j-W[i]] + V[i], B[i+1][j]);
11. B[1][C] = max(B[2][C], v[1] + B[2][C-W[i]]);
12. return B;
构造最优解
构造最优解的信息同样也保存在了 B [ 1 : n ] [ 0 : C ] B[1:n][0:C] B[1:n][0:C]中。事实上,如果 B [ 1 ] [ C ] = B [ 2 ] [ C ] B[1][C] = B[2][C] B[1][C]=B[2][C],说明第一个物品不在最优解中,依次类推。
0-1背包空间优化
参考。
空间优化,每一次
b
i
,
j
b_{i,j}
bi,j 的计算只与前一行的
b
i
−
1
,
j
b_{i-1,j}
bi−1,j 和
b
i
−
1
,
j
−
W
i
b_{i-1,j-W_i}
bi−1,j−Wi 相关,因此可以将
d
p
dp
dp 缩小成一维数组,从而达到优化空间的目的,状态转移方程变为:
b
j
=
m
a
x
{
b
j
,
b
j
−
W
i
+
V
i
}
b_{j} = max\{\ b_{j},\ b_{j-W_i} + V_i\ \}
bj=max{ bj, bj−Wi+Vi }
注意,状态转移方程中 b j b_j bj 需要用到它前面的值 b j − W i b_{j - W_i} bj−Wi,所以一维数组中代价 j j j 的扫描顺序应该是从大到小( C C C 到 0 0 0),否则处理前一个物品时保留下来的值将会被修改,从而造成错误。
动态规划总结
前面几个例子展示了动态规划求解问题的基本步骤,即先分析问题的优化子结构和重叠子问题,然后递归地定义最优解(或解的代价),再自底向上地求解所有相关子问题,最后构造最优解。
另一方面,我们不遗余力地强调动态规划算法设计的自然过程,即面对待求解的计算问题,尝试使用分治策略将问题求解转换为处理子问题,分析如何利用子问题的解来构造原问题的解从而产生优化子结构;再自然地基于简单的分治方法来处理,说明问题具有重叠子问题;继而分析分治过程中产生的子问题的空间,根据子问题的空间大小和形式来定义问题的解的代价,设计数据结构和计算顺序,最后得到求解问题的动态规划算法。而且,动态规划算法中的数据结构总是按需选用的,而非人为臆造;自底向上的计算次序也是根据数据结构和代价递归式自然地产生的,且自然地对应了按子问题规模递增的顺序求解所有子问题的顺序。
分析优化子结构和重叠子问题时,可以自顶向下地分析;在递归定义最优解或解的代价时,就需要自底向上的定义。