动态规划
1. 概念
动态规划被广泛地用于求解组合最优化问题,它并不是递归地调用自身,虽然问题的基础解通常是用递归函数的形式来说明的。与分治算法的设计思路不同,动态规划采取自底向上的方式递推求值,并把中间结果存储起来以便以后用来计算所需要求的解。
因此,使用动态规划算法可以高效解决许多组合最优化问题,也可以改善暴力搜索算法的时间复杂度。下面我们根据一个例子来简单了解一下动态规划:
————————————
e
x
a
m
p
l
e
_
0
example\_0
example_0—————————————
了解过递归和归纳方法的同学肯定都很熟悉
F
i
b
o
n
a
c
c
i
Fibonacci
Fibonacci 序列问题,即
f
1
=
1
,
f
2
=
1
,
f
3
=
2
,
f
4
=
3
,
…
f_1=1,f_2=1,f_3=2,f_4=3,\dots
f1=1,f2=1,f3=2,f4=3,…,序列中每一个数是其前两个数之和,因此我们可以得到该序列的递归定义:
f
(
n
)
=
{
1
i
f
n
=
1
o
r
n
=
2
f
(
n
−
1
)
+
f
(
n
−
2
)
i
f
n
⩾
3
f(n)=\begin{cases}1\hspace{1cm}if\space\space n=1\space\space or\space\space n=2\\\\ f(n-1)+f(n-2)\hspace{1cm}if\space\space n\geqslant3\end{cases}
f(n)=⎩⎪⎨⎪⎧1if n=1 or n=2f(n−1)+f(n−2)if n⩾3,可见求解
F
i
b
o
n
a
c
c
i
Fibonacci
Fibonacci 序列问题的递归形式
- 1. p r o c e d u r e f ( n ) \hspace{1cm}1.\space procedure\space\space f(n) 1. procedure f(n)
- 2. i f ( n = 1 ) o r ( n = 2 ) t h e n r e t u r n 1 \hspace{1cm}2.\space if\space\space(n=1)\space\space or\space\space(n=2)\space\space then\space\space return\space\space1 2. if (n=1) or (n=2) then return 1
- 3. e l s e r e t u r n f ( n − 1 ) + f ( n − 2 ) \hspace{1cm}3.\space else\space\space return\space\space f(n-1)+f(n-2) 3. else return f(n−1)+f(n−2)
这种递归形式是许多递归算法的核心思路,它的优点在于能够将一些复杂的算法用递归的形式简洁地表示出来,但是它的缺点也很明显,有时候递归思路能够给出一个极其有效的算法,有时候却会因重复调用而产生巨大的浪费,例如我们现在将
F
i
b
o
n
a
c
c
i
Fibonacci
Fibonacci 序列的递推式做适当的展开:
f
(
n
)
=
f
(
n
−
1
)
+
f
(
n
−
2
)
f(n)=f(n-1)+f(n-2)
f(n)=f(n−1)+f(n−2)
=
2
f
(
n
−
2
)
+
f
(
n
−
3
)
\hspace{0.78cm}=2f(n-2)+f(n-3)
=2f(n−2)+f(n−3)
=
3
f
(
n
−
3
)
+
2
f
(
n
−
4
)
\hspace{0.78cm}=3f(n-3)+2f(n-4)
=3f(n−3)+2f(n−4)
=
5
f
(
n
−
4
)
+
3
f
(
n
−
5
)
\hspace{0.78cm}=5f(n-4)+3f(n-5)
=5f(n−4)+3f(n−5)
这里假设计算
f
(
1
)
f(1)
f(1) 和
f
(
2
)
f(2)
f(2) 需要一个单位的时间,那么我们可以得到上述过程的时间复杂度为
T
(
n
)
=
{
1
i
f
n
=
1
o
r
n
=
2
T
(
n
−
1
)
+
T
(
n
−
2
)
i
f
n
⩾
3
T(n)=\begin{cases}1\hspace{1cm}if\space\space n=1\space\space or\space\space n=2\\\\ T(n-1)+T(n-2)\hspace{1cm}if\space\space n\geqslant3\end{cases}
T(n)=⎩⎪⎨⎪⎧1if n=1 or n=2T(n−1)+T(n−2)if n⩾3,进而可知
T
(
n
)
=
f
(
n
)
T(n)=f(n)
T(n)=f(n)。那么当
n
n
n 很大时,根据相关结论,有
f
(
n
)
≈
Θ
(
ϕ
n
)
f(n)\approx\Theta(\phi^n)
f(n)≈Θ(ϕn),其中
ϕ
=
1
+
5
2
≈
1.61803
\phi=\frac{1+\sqrt{5}}{2}\approx1.61803
ϕ=21+5≈1.61803,即计算
f
(
n
)
f(n)
f(n) 所耗费的时间是指数增长的。但是,我们可以想一想,之所以产生这么大的浪费,根源是我们是自顶向下进行的递归,但是如果我们换一种思路,从
f
1
,
f
2
f_1,f_2
f1,f2 开始自底向上地计算直至
f
n
f_n
fn,那么每一个值只需要计算一次,即只需要
Θ
(
n
)
\Theta(n)
Θ(n) 时间和
Θ
(
1
)
\Theta(1)
Θ(1) 空间,时间复杂度大大降低。
—————————————————————————————————
到这里相信大家对自底向上的优势已经有了初步的认识,下面我们结合例子来看一下动态规划的基本思想:
- 动态规划的核心就是分治和消除冗余
- 所谓“分治”,即将问题实例分解为更小的、相似的字问题。在上述例子中表现为 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n)=f(n-1)+f(n-2) f(n)=f(n−1)+f(n−2)。
- 所谓“消除冗余”,即将子问题的解存储起来,从而避免重复运算相同的字问题而造成浪费。在上述例子中,冗余表现为计算 f ( n ) f(n) f(n) 需要计算 f ( n − 1 ) f(n-1) f(n−1) 和 f ( n − 2 ) f(n-2) f(n−2) 的,但是为了得到 f ( n − 1 ) f(n-1) f(n−1) 的值,我们又计算了 f ( n − 2 ) f(n-2) f(n−2) 和 f ( n − 3 ) f(n-3) f(n−3) 的值,这里我们就计算了两次 f ( n − 2 ) f(n-2) f(n−2) 的值,这样不断递归下去,当 n n n 的值非常大时,因为重复运算而造成的浪费是很可怕的。
此时,我们可以初步得到动态规划算法的基本步骤:
- 找出最优解的性质,并刻划其结构特征
- 递归地定义最优值
- 以自底向上的方式计算出最优值
- 根据计算最优值时得到的信息,构造最优解
2. 简单运用
动态规划在许多算法中都扮演着不可或缺的角色,这里我们列举一些典型算法,一起体会一下动态规划的奥妙之处。
最长公共子序列问题
————————————
e
x
a
m
p
l
e
_
1
example\_1
example_1——————————————
在字母表
∑
\sum
∑ 上,分别给出两个长度为
n
n
n 和
m
m
m 的字符串
A
,
B
A,B
A,B,确定在
A
A
A 和
B
B
B 中最长公共子序列的长度。所谓子序列,即假设字符串
A
=
a
1
a
2
⋯
a
n
A=a_1a_2\cdots a_n
A=a1a2⋯an,若有字符串
A
′
=
a
i
1
a
i
2
⋯
a
i
k
A'=a_{i_1}a_{i_2}\cdots a_{i_k}
A′=ai1ai2⋯aik,其中每个
i
j
i_j
ij 都在
1
1
1 和
n
n
n 之间,且
1
⩽
i
1
<
i
2
<
⋯
<
i
k
⩽
n
1\leqslant i_1<i_2<\cdots<i_k\leqslant n
1⩽i1<i2<⋯<ik⩽n,则称
A
′
A'
A′ 是
A
A
A 的一个子序列。下面我们根据一个例子来简单理解一下最长公共子序列问题:
现在有
∑
=
{
x
,
y
,
z
}
,
A
=
z
x
y
x
y
z
,
B
=
x
y
y
z
x
\sum=\{x,y,z\},A=zxyxyz,B=xyyzx
∑={x,y,z},A=zxyxyz,B=xyyzx,根据子序列的定义,
x
y
y
xyy
xyy 是
A
A
A 的子序列,同时也是
B
B
B 的子序列,那么我们可以将
x
y
y
xyy
xyy 称为
A
A
A 和
B
B
B 的公共子序列,易知其长度为
3
3
3。然而,它并不是
A
A
A 和
B
B
B 的最长公共子序列,因为字符串
x
y
y
z
xyyz
xyyz 也是
A
A
A 和
B
B
B 的子序列,并且它的长度为
4
4
4。同时,由于
A
A
A 和
B
B
B 没有比
4
4
4 更长的公共子序列,因此这道题的解为
4
4
4。
这一问题的传统算法自然是直接进行暴力搜索:列举
A
A
A 中所有的
2
n
2^n
2n 个子序列,对于每一个子序列再在
Θ
(
m
)
\Theta(m)
Θ(m) 的时间内确定它是否是
B
B
B 的子序列,显然此算法的时间复杂度为
Θ
(
m
2
n
)
\Theta(m2^n)
Θ(m2n),是指数复杂性的,一般来说很难接受。这里我们来回顾一下动态规划:
首先,我们需要找到一个求解最长公共子序列长度的递推公式,令
A
=
a
1
a
2
⋯
a
n
A=a_1a_2\cdots a_n
A=a1a2⋯an 和
B
=
b
1
b
2
⋯
b
m
B=b_1b_2\cdots b_m
B=b1b2⋯bm,此外,令
L
[
i
,
j
]
L[i,j]
L[i,j] 表示
a
1
a
2
⋯
a
i
a_1a_2\cdots a_i
a1a2⋯ai 和
b
1
b
2
⋯
b
j
b_1b_2\cdots b_j
b1b2⋯bj 的最长公共子序列的长度,注意
i
,
j
i,j
i,j 都有可能是
0
0
0,即
a
1
a
2
⋯
a
i
a_1a_2\cdots a_i
a1a2⋯ai 和
b
1
b
2
⋯
b
j
b_1b_2\cdots b_j
b1b2⋯bj 中的一个或同时为空字符串,那么我们可以得到求解
A
,
B
A,B
A,B 的最长公共子序列长度的递推式:
L
[
i
,
j
]
=
{
0
i
f
i
=
0
o
r
j
=
0
L
[
i
−
1
,
j
−
1
]
+
1
i
f
i
>
0
,
j
>
0
a
n
d
a
i
=
b
j
max
{
L
[
i
,
j
−
1
]
,
L
[
i
−
1
,
j
]
}
i
f
i
>
0
,
j
>
0
a
n
d
a
i
≠
b
j
L[i,j]=\begin{cases}0\hspace{1cm}if\space\space i=0\space\space or\space\space j=0\\\\L[i-1,j-1]+1\hspace{1cm}if\space\space i>0,j>0\space\space and\space\space a_i=b_j\\\\\max\{L[i,j-1],L[i-1,j]\}\hspace{1cm}if\space\space i>0,j>0\space\space and\space\space a_i\neq b_j\end{cases}
L[i,j]=⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧0if i=0 or j=0L[i−1,j−1]+1if i>0,j>0 and ai=bjmax{L[i,j−1],L[i−1,j]}if i>0,j>0 and ai=bj
此时,根据动态规划自底向上的思路,对于每一对
i
i
i 和
j
j
j 的值,
0
⩽
i
⩽
n
,
0
⩽
j
⩽
m
0\leqslant i\leqslant n,0\leqslant j\leqslant m
0⩽i⩽n,0⩽j⩽m,我们用一个
(
n
+
1
)
×
(
m
+
1
)
(n+1)\times(m+1)
(n+1)×(m+1) 的表来计算并存储
L
[
i
,
j
]
L[i,j]
L[i,j] 的值,进而得到算法
L
C
S
LCS
LCS:
- 输入:字母表 ∑ \sum ∑ 上的两个字符串 A , B A,B A,B,长度分别为 n n n 和 m m m
- 输出: A A A 和 B B B 最长公共子序列的长度
- 1. f o r i ← 0 t o n \hspace{1cm}1.\space for\space\space i\leftarrow0\space\space to\space\space n 1. for i←0 to n
- 2. L [ i , 0 ] ← 0 \hspace{1cm}2.\space\hspace{1cm}L[i,0]\leftarrow0 2. L[i,0]←0
- 3. e n d f o r \hspace{1cm}3.\space end\space for 3. end for
- 4. f o r j ← 0 t o m \hspace{1cm}4.\space for\space\space j\leftarrow0\space\space to\space\space m 4. for j←0 to m
- 5. L [ 0 , j ] ← 0 \hspace{1cm}5.\space\hspace{1cm}L[0,j]\leftarrow0 5. L[0,j]←0
- 6. e n d f o r \hspace{1cm}6.\space end\space for 6. end for
- 7. f o r i ← 1 t o n \hspace{1cm}7.\space for\space\space i\leftarrow1\space\space to\space\space n 7. for i←1 to n
- 8. f o r j ← 1 t o m \hspace{1cm}8.\space\hspace{1cm}for\space\space j\leftarrow1\space\space to\space\space m 8. for j←1 to m
- 9. i f a i = b j t h e n L [ i , j ] ← L [ i − 1 , j − 1 ] + 1 \hspace{1cm}9.\space\hspace{2cm}if\space\space a_i=b_j\space\space then\space\space L[i,j]\leftarrow L[i-1,j-1]+1 9. if ai=bj then L[i,j]←L[i−1,j−1]+1
- 10. e l s e L [ i , j ] ← max { L [ i , j − 1 ] , L [ i − 1 , j ] } \hspace{0.85cm}10.\space\hspace{2cm}else\space\space L[i,j]\leftarrow\max\{L[i,j-1],L[i-1,j]\} 10. else L[i,j]←max{L[i,j−1],L[i−1,j]}
- 11. e n d i f \hspace{0.85cm}11.\space\hspace{2cm}end\space if 11. end if
- 12. e n d f o r \hspace{0.85cm}12.\space\hspace{1cm}end\space for 12. end for
- 13. e n d f o r \hspace{0.85cm}13.\space end\space for 13. end for
- 14. r e t u r n L [ n , m ] \hspace{0.85cm}14.\space return\space\space L[n,m] 14. return L[n,m]
显然,由于计算表的每项输入需要
Θ
(
1
)
\Theta(1)
Θ(1) 时间,那么算法的时间复杂度正好是表的大小
Θ
(
n
m
)
\Theta(nm)
Θ(nm)。此外,易知最长公共子序列问题的最优解只需要耗费
Θ
(
min
{
m
,
n
}
)
\Theta(\min\{m,n\})
Θ(min{m,n}) 的空间。
—————————————————————————————————
矩阵链相乘
————————————
e
x
a
m
p
l
e
_
2
example\_2
example_2——————————————
假设我们要用标准的矩阵乘法来计算
M
1
,
M
2
,
M
3
M_1,M_2,M_3
M1,M2,M3 三个矩阵的乘积,这三个矩阵的规格分别为
2
×
10
,
10
×
,
2
×
10
2\times10,10\times,2\times10
2×10,10×,2×10。现在如果我们先把
M
1
M_1
M1 和
M
2
M_2
M2 相乘,然后把结果与
M
3
M_3
M3 相乘,显然需要执行
2
×
10
×
2
+
2
×
2
×
10
=
80
2\times10\times2+2\times2\times10=80
2×10×2+2×2×10=80 次乘法操作;如果使用结合率,用
M
1
M_1
M1 去乘
M
2
,
M
3
M_2,M_3
M2,M3 的乘积,那么就需要执行
10
×
2
×
10
+
2
×
10
×
10
=
400
10\times2\times10+2\times10\times10=400
10×2×10+2×10×10=400 次乘法操作,易知执行乘法
M
1
(
M
2
M
3
)
M_1(M_2M_3)
M1(M2M3) 耗费的时间是执行乘法
(
M
1
M
2
)
M
3
(M_1M_2)M_3
(M1M2)M3 的
5
5
5 倍。
因此我们知道,一般来说,
n
n
n 个矩阵
M
1
M
2
⋯
M
n
M_1M_2\cdots M_n
M1M2⋯Mn 链乘法的耗费,取决于
n
−
1
n-1
n−1 个乘法执行的次序,因此,矩阵链相乘问题就是找到
M
1
M
2
⋯
M
n
M_1M_2\cdots M_n
M1M2⋯Mn 按照何种结合方式相乘,所需要的乘法次数最小。同样的,最传统的算法就是暴力搜索:
- 找出所有可能的相乘结合方式;
- 计算每种相乘结合方式所需要的乘法次数;
- 求 min \min min
举个例子,现在有
4
4
4 个矩阵
M
1
,
M
2
,
M
3
,
M
4
M_1,M_2,M_3,M_4
M1,M2,M3,M4,暴力搜索算法将试算所有可能的相乘结合方式,在该例子中,一共是
5
5
5 中顺序:
(
M
1
(
M
2
(
M
3
M
4
)
)
)
(M_1(M_2(M_3M_4)))
(M1(M2(M3M4)))
(
M
1
(
(
M
2
M
3
)
M
4
)
)
(M_1((M_2M_3)M_4))
(M1((M2M3)M4))
(
(
M
1
M
2
)
(
M
3
M
4
)
)
((M_1M_2)(M_3M_4))
((M1M2)(M3M4))
(
(
(
M
1
M
2
)
M
3
)
M
4
)
(((M_1M_2)M_3)M_4)
(((M1M2)M3)M4)
(
(
M
1
(
M
2
M
3
)
)
M
4
)
((M_1(M_2M_3))M_4)
((M1(M2M3))M4)
一般来说,顺序数等于乘这
n
n
n 个矩阵时用每一种可能的途径放置小括号的方法数。设
f
(
n
)
f(n)
f(n) 是求
n
n
n 个矩阵乘积的所有放置小括号的方法数,假定要进行乘法
(
M
1
M
2
⋯
M
k
)
×
(
M
k
+
1
M
k
+
2
⋯
M
n
)
(M_1M_2\cdots M_k)\times(M_{k+1}M_{k+2}\cdots M_n)
(M1M2⋯Mk)×(Mk+1Mk+2⋯Mn),那么对于前
k
k
k 个矩阵有
f
(
k
)
f(k)
f(k) 种方法放置小括号,对于
f
(
k
)
f(k)
f(k) 中的每一种方法,余下的
n
−
k
n-k
n−k 个矩阵另有
f
(
n
−
k
)
f(n-k)
f(n−k) 种放置小括号的方法,因此总计
f
(
k
)
f
(
n
−
k
)
f(k)f(n-k)
f(k)f(n−k) 种方法。由于
k
k
k 是介于
1
1
1 和
n
−
1
n-1
n−1 中的任意值,我们可以得到
n
n
n 个矩阵放置小括号的所有方法数:
f
(
n
)
=
∑
k
=
1
n
−
1
f
(
k
)
f
(
n
−
k
)
f(n)=\sum_{k=1}^{n-1}f(k)f(n-k)
f(n)=∑k=1n−1f(k)f(n−k)。
显然,两个矩阵相乘只有一种方法,三个矩阵相乘则有两种方法。因此,易知
f
(
2
)
=
1
,
f
(
3
)
=
2
f(2)=1,f(3)=2
f(2)=1,f(3)=2,为了使递推式有意义,我们人为地规定
f
(
1
)
=
1
f(1)=1
f(1)=1,进而得到
f
(
n
)
=
1
n
(
n
−
1
2
n
−
2
)
=
(
2
n
−
2
)
!
n
(
(
n
−
1
)
!
)
2
f(n)=\frac{1}{n}(_{\space n-1}^{2n-2})=\frac{(2n-2)!}{n((n-1)!)^2}
f(n)=n1( n−12n−2)=n((n−1)!)2(2n−2)!,已知
n
!
≈
2
π
n
(
n
e
)
n
,
e
=
2.71828
⋯
n!\approx\sqrt{2\pi n}(\frac{n}{e})^n,e=2.71828\cdots
n!≈2πn(en)n,e=2.71828⋯
⇒
f
(
n
)
=
(
2
n
−
2
)
!
n
(
(
n
−
1
)
!
)
2
≈
4
n
4
π
n
1.5
\Rightarrow f(n)=\frac{(2n-2)!}{n((n-1)!)^2}\approx\frac{4^n}{4\sqrt{\pi}n^{1.5}}
⇒f(n)=n((n−1)!)2(2n−2)!≈4πn1.54n
⇒
f
(
n
)
=
Ω
(
4
n
n
1.5
)
\Rightarrow f(n)=\Omega(\frac{4^n}{n^{1.5}})
⇒f(n)=Ω(n1.54n)
由于对于每个小括号化表达式,找到数量乘法次数的时间耗费是
Θ
(
n
)
\Theta(n)
Θ(n),这样用暴力搜索算法可以求得找到
n
n
n 个矩阵相乘的最优方法所需的运行时间是
Ω
(
4
n
n
)
\Omega(\frac{4^n}{\sqrt{n}})
Ω(n4n),这个时间复杂度是无法接受的。
但是,如果我们能够导出一个求解乘法的最少次数的递推关系式,就可以利用动态规划设计出一个更加高效的算法。由于对于每个
i
,
1
⩽
i
<
n
i,1\leqslant i<n
i,1⩽i<n,矩阵
M
i
M_i
Mi 的列数一定等于
M
i
+
1
M_{i+1}
Mi+1 的行数(否则不符合矩阵乘法的前提),因此指定每个矩阵的行数末尾矩阵
M
n
M_n
Mn 的列数就足够了。
假设现在有
n
+
1
n+1
n+1 维数
r
1
,
r
2
,
…
,
r
n
+
1
r_1,r_2,\dots,r_{n+1}
r1,r2,…,rn+1,这里
r
i
,
r
i
+
1
r_i,r_{i+1}
ri,ri+1 分别是矩阵
M
i
M_i
Mi 的行数和列数,
1
⩽
i
⩽
n
1\leqslant i\leqslant n
1⩽i⩽n。这里,我们使用
M
i
,
j
M_{i,j}
Mi,j 来记录
M
i
M
i
+
1
⋯
M
j
M_iM_{i+1}\cdots M_j
MiMi+1⋯Mj 的乘积。此外,假设矩阵链
M
i
,
j
M_{i,j}
Mi,j 的乘法的耗费用数量乘法的次数来测度,记为
C
[
i
,
j
]
C[i,j]
C[i,j]。对于给定的一对索引
i
,
j
,
1
⩽
i
<
j
⩽
n
,
M
i
,
j
i,j,1\leqslant i<j\leqslant n,M_{i,j}
i,j,1⩽i<j⩽n,Mi,j 的计算方法为:
设
k
k
k 是
i
+
1
i+1
i+1 和
j
j
j 之间的一个索引,计算两个矩阵
M
i
,
k
−
1
=
M
i
M
i
+
1
⋯
M
k
−
1
M_{i,k-1}=M_iM_{i+1}\cdots M_{k-1}
Mi,k−1=MiMi+1⋯Mk−1 和
M
k
,
j
=
M
k
M
k
+
1
⋯
M
j
M_{k,j}=M_kM_{k+1}\cdots M_j
Mk,j=MkMk+1⋯Mj,那么
M
i
.
j
=
M
i
,
k
−
1
M
k
,
j
M_{i.j}=M_{i,k-1}M_{k,j}
Mi.j=Mi,k−1Mk,j。显然,用这种方式计算
M
i
,
j
M_{i,j}
Mi,j 的总耗费,是计算
M
i
,
k
−
1
M_{i,k-1}
Mi,k−1 的耗费加上计算
M
k
,
j
M_{k,j}
Mk,j 的耗费,在加上
M
i
,
k
−
1
×
M
k
,
j
M_{i,k-1}\times M_{k,j}
Mi,k−1×Mk,j 的耗费(
r
i
r
k
r
j
+
1
r_ir_kr_{j+1}
rirkrj+1),即有
C
[
i
,
j
]
=
min
i
<
k
⩽
j
{
C
[
i
,
k
−
1
]
+
C
[
k
,
j
]
+
r
i
r
k
r
j
+
1
}
C[i,j]=\min_{i<k\leqslant j}\{C[i,k-1]+C[k,j]+r_ir_kr_{j+1}\}
C[i,j]=mini<k⩽j{C[i,k−1]+C[k,j]+rirkrj+1}
因此,对于找出执行矩阵乘法
M
1
M
2
⋯
M
n
M_1M_2\cdots M_n
M1M2⋯Mn 所需要数量乘法的最小次数,本质上就是求解如下递推式:
C
[
1
,
n
]
=
min
1
<
k
⩽
n
{
C
[
1.
k
−
1
]
+
C
[
k
,
n
]
+
r
1
r
k
r
n
+
1
}
C[1,n]=\min_{1<k\leqslant n}\{C[1.k-1]+C[k,n]+r_1r_kr_{n+1}\}
C[1,n]=min1<k⩽n{C[1.k−1]+C[k,n]+r1rkrn+1}
但是,如果我们直接自顶向下求解递推式,同样会导致巨大数量的重复递归调用,进而浪费资源,因此,我们需要采用动态规划自底向上的思路来消除冗余。最终得到算法
m
a
t
c
h
a
i
n
matchain
matchain:
- 输入: r [ 1 ⋯ n + 1 ] r[1\cdots n+1] r[1⋯n+1],表示 n n n 个矩阵规模的 n + 1 n+1 n+1 个整数
- 输出: n n n 个矩阵连乘的最小乘法次数
- 1. f o r i ← 1 t o n { \hspace{1cm}1.\space for\space\space i\leftarrow1\space\space to\space\space n\space\space\{ 1. for i←1 to n {填充对角线 d 0 } d_0\} d0}
- 2. C [ i , j ] ← 0 \hspace{1cm}2.\space\hspace{1cm}C[i,j]\leftarrow0 2. C[i,j]←0
- 3. e n d f o r \hspace{1cm}3.\space end\space for 3. end for
- 4. f o r d ← 1 t o n − 1 { \hspace{1cm}4.\space for\space\space d\leftarrow1\space\space to\space\space n-1\space\space\{ 4. for d←1 to n−1 {填充对角线 d 1 d_1 d1 到 d n − 1 } d_{n-1}\} dn−1}
- 5. f o r i ← 1 t o n − d { \hspace{1cm}5.\space\hspace{1cm}for\space\space i\leftarrow1\space\space to\space\space n-d\space\space\{ 5. for i←1 to n−d {填充对角线 d i d_i di 的每个项目 } \} }
- 6. j ← i + d { \hspace{1cm}6.\space\hspace{2cm}j\leftarrow i+d\space\space\{ 6. j←i+d {该对角线上 j , i j,i j,i 满足的关系 } \} }
- 7. C [ i . j ] ← ∞ \hspace{1cm}7.\space\hspace{2cm}C[i.j]\leftarrow\infty 7. C[i.j]←∞
- 8. f o r k ← i + 1 t o j \hspace{1cm}8.\space\hspace{2cm}for\space\space k\leftarrow i+1\space\space to\space\space j 8. for k←i+1 to j
- 9. C [ i , j ] ← min { C [ i , j ] , C [ i , k − 1 ] + C [ k , j ] + r i × r k × r j + 1 } \hspace{1cm}9.\space\hspace{3cm}C[i,j]\leftarrow\min\{C[i,j],C[i,k-1]+C[k,j]+r_i\times r_k\times r_{j+1}\} 9. C[i,j]←min{C[i,j],C[i,k−1]+C[k,j]+ri×rk×rj+1}
- 10. e n d f o r \hspace{0.85cm}10.\space\hspace{2cm}end\space for 10. end for
- 11. e n d f o r \hspace{0.85cm}11.\space\hspace{1cm}end\space for 11. end for
- 12. e n d f o r \hspace{0.85cm}12.\space end\space for 12. end for
- 13. r e t u r n C [ 1 , n ] \hspace{0.85cm}13.\space return\space\space C[1,n] 13. return C[1,n]
⇒
T
(
n
)
=
∑
d
=
1
n
−
1
∑
i
=
1
n
−
d
∑
k
=
i
+
1
j
c
\Rightarrow T(n)=\sum_{d=1}^{n-1}\sum_{i=1}^{n-d}\sum_{k=i+1}^{j}c
⇒T(n)=∑d=1n−1∑i=1n−d∑k=i+1jc
=
∑
d
=
1
n
−
1
∑
i
=
1
n
−
d
∑
k
=
i
+
1
i
+
d
c
\hspace{1.27cm}=\sum_{d=1}^{n-1}\sum_{i=1}^{n-d}\sum_{k=i+1}^{i+d}c
=∑d=1n−1∑i=1n−d∑k=i+1i+dc
=
∑
d
=
1
n
−
1
∑
i
=
1
n
−
d
∑
k
=
1
d
c
\hspace{1.27cm}=\sum_{d=1}^{n-1}\sum_{i=1}^{n-d}\sum_{k=1}^dc
=∑d=1n−1∑i=1n−d∑k=1dc
=
∑
d
=
1
n
−
1
∑
i
=
1
n
−
d
c
d
\hspace{1.27cm}=\sum_{d=1}^{n-1}\sum_{i=1}^{n-d}cd
=∑d=1n−1∑i=1n−dcd
=
c
∑
d
=
1
n
−
1
∑
i
=
1
n
−
d
d
\hspace{1.27cm}=c\sum_{d=1}^{n-1}\sum_{i=1}^{n-d}d
=c∑d=1n−1∑i=1n−dd
=
c
(
∑
i
=
1
n
−
1
1
+
∑
i
=
1
n
−
2
2
+
∑
i
=
1
n
−
3
3
+
⋯
+
∑
i
=
1
n
−
(
n
−
1
)
(
n
−
1
)
)
\hspace{1.27cm}=c\big(\sum_{i=1}^{n-1}1+\sum_{i=1}^{n-2}2+\sum_{i=1}^{n-3}3+\cdots+\sum_{i=1}^{n-(n-1)}(n-1)\big)
=c(∑i=1n−11+∑i=1n−22+∑i=1n−33+⋯+∑i=1n−(n−1)(n−1))
=
c
(
(
n
−
1
)
⋅
1
+
(
n
−
2
)
⋅
2
+
(
n
−
3
)
⋅
3
+
⋯
+
(
n
−
(
n
−
1
)
)
⋅
(
n
−
1
)
)
\hspace{1.27cm}=c\big((n-1)\cdot1+(n-2)\cdot2+(n-3)\cdot3+\cdots+(n-(n-1))\cdot(n-1)\big)
=c((n−1)⋅1+(n−2)⋅2+(n−3)⋅3+⋯+(n−(n−1))⋅(n−1))
=
c
(
n
⋅
1
+
n
⋅
2
+
n
⋅
3
+
⋯
+
n
⋅
(
n
−
1
)
−
1
⋅
1
−
2
⋅
2
−
(
n
−
1
)
⋅
(
n
−
1
)
)
\hspace{1.27cm}=c\big(n\cdot1+n\cdot2+n\cdot3+\cdots+n\cdot(n-1)-1\cdot1-2\cdot2-(n-1)\cdot(n-1)\big)
=c(n⋅1+n⋅2+n⋅3+⋯+n⋅(n−1)−1⋅1−2⋅2−(n−1)⋅(n−1))
=
c
(
n
(
1
+
2
+
3
+
⋯
+
(
n
−
1
)
)
−
∑
k
=
1
n
−
1
k
2
)
\hspace{1.27cm}=c\big(n(1+2+3+\cdots+(n-1))-\sum_{k=1}^{n-1}k^2\big)
=c(n(1+2+3+⋯+(n−1))−∑k=1n−1k2)
=
c
(
n
⋅
n
(
n
−
1
)
2
−
1
6
(
n
−
1
)
n
(
2
n
−
1
)
)
\hspace{1.27cm}=c\big(n\cdot\frac{n(n-1)}{2}-\frac{1}{6}(n-1)n(2n-1)\big)
=c(n⋅2n(n−1)−61(n−1)n(2n−1))
=
1
6
(
c
n
3
−
c
n
)
=
Θ
(
n
3
)
\hspace{1.27cm}=\frac{1}{6}(cn^3-cn)=\Theta(n^3)
=61(cn3−cn)=Θ(n3)
这个算法的思路可能没那么好理解,但是可以来看下面的一个实例分析:
已知矩阵链
(
M
1
)
5
×
10
⋅
(
M
2
)
10
×
4
⋅
(
M
3
)
4
×
6
⋅
(
M
4
)
6
×
10
⋅
(
M
5
)
10
×
2
(M_1)_{5\times10}\cdot(M_2)_{10\times4}\cdot(M_3)_{4\times6}\cdot(M_4)_{6\times10}\cdot(M_5)_{10\times2}
(M1)5×10⋅(M2)10×4⋅(M3)4×6⋅(M4)6×10⋅(M5)10×2
所以有
r
1
=
5
,
r
2
=
10
,
r
3
=
4
,
r
4
=
6
,
r
5
=
10
,
r
6
=
2
r_1=5,r_2=10,r_3=4,r_4=6,r_5=10,r_6=2
r1=5,r2=10,r3=4,r4=6,r5=10,r6=2
d = 0 d=0 d=0 | d = 1 d=1 d=1 | d = 2 d=2 d=2 | d = 3 d=3 d=3 | d = 4 d=4 d=4 |
---|---|---|---|---|
C [ 1 , 1 ] = 0 ( M 1 ) C[1,1]=0\space\space(M_1) C[1,1]=0 (M1) | C [ 1 , 2 ] = 200 ( M 1 M 2 ) C[1,2]=200\space\space(M_1M_2) C[1,2]=200 (M1M2) | C [ 1 , 3 ] = 320 C[1,3]=320 C[1,3]=320 | C [ 1 , 4 ] = 620 C[1,4]=620 C[1,4]=620 | C [ 1 , 5 ] = 348 C[1,5]=348 C[1,5]=348 |
C [ 2 , 2 ] = 0 ( M 2 ) C[2,2]=0\space\space(M_2) C[2,2]=0 (M2) | C [ 2 , 3 ] = 240 ( M 2 M 3 ) C[2,3]=240\space\space(M_2M_3) C[2,3]=240 (M2M3) | C [ 2 , 4 ] = 640 ( M 2 ) ( M 2 M 3 ) C[2,4]=640\space\space(M_2)(M_2M_3) C[2,4]=640 (M2)(M2M3) | C [ 2 , 5 ] = 248 C[2,5]=248 C[2,5]=248 | |
C [ 3 , 3 ] = 0 ( M 3 ) C[3,3]=0\space\space(M_3) C[3,3]=0 (M3) | C [ 3 , 4 ] = 240 ( M 3 M 4 ) C[3,4]=240\space\space(M_3M_4) C[3,4]=240 (M3M4) | C [ 3 , 5 ] = 168 C[3,5]=168 C[3,5]=168 | ||
C [ 4 , 4 ] = 0 ( M 4 ) C[4,4]=0\space\space(M_4) C[4,4]=0 (M4) | C [ 4 , 5 ] = 120 ( M 4 M 5 ) C[4,5]=120\space\space(M_4M_5) C[4,5]=120 (M4M5) | |||
C [ 5 , 5 ] = 0 ( M 5 ) C[5,5]=0\space\space(M_5) C[5,5]=0 (M5) |
d
=
0
d=0
d=0:
C
[
1
,
1
]
−
C
[
2
,
2
]
−
C
[
3
,
3
]
−
C
[
4
,
4
]
−
C
[
5
,
5
]
C[1,1]-C[2,2]-C[3,3]-C[4,4]-C[5,5]
C[1,1]−C[2,2]−C[3,3]−C[4,4]−C[5,5]
d
=
1
d=1
d=1:
C
[
1
,
2
]
−
C
[
2
,
3
]
−
C
[
3
,
4
]
−
C
[
4
,
5
]
C[1,2]-C[2,3]-C[3,4]-C[4,5]
C[1,2]−C[2,3]−C[3,4]−C[4,5]
d
=
2
d=2
d=2:
C
[
1
,
3
]
−
C
[
2
,
4
]
−
C
[
3
,
5
]
C[1,3]-C[2,4]-C[3,5]
C[1,3]−C[2,4]−C[3,5]
d
=
3
d=3
d=3:
C
[
1
,
4
]
−
C
[
2
,
5
]
C[1,4]-C[2,5]
C[1,4]−C[2,5]
d
=
4
d=4
d=4:
C
[
1
,
5
]
C[1,5]
C[1,5]
这里我们简单看一下
C
[
2
,
4
]
C[2,4]
C[2,4] 的计算过程:
C
[
2
,
4
]
=
min
2
<
k
⩽
4
{
C
[
2
,
k
−
1
]
+
C
[
k
,
4
]
+
r
2
r
k
r
4
+
1
}
C[2,4]=\min_{2<k\leqslant4}\{C[2,k-1]+C[k,4]+r_2r_kr_{4+1}\}
C[2,4]=min2<k⩽4{C[2,k−1]+C[k,4]+r2rkr4+1}
k
=
3
→
C
[
2
,
4
]
=
C
[
2
,
2
]
+
C
[
3
,
4
]
+
r
2
r
3
r
5
=
0
+
240
+
10
×
4
×
10
=
640
→
(
M
2
)
(
M
3
M
4
)
k=3\rightarrow C[2,4]=C[2,2]+C[3,4]+r_2r_3r_5=0+240+10\times4\times10=640\rightarrow(M_2)(M_3M_4)
k=3→C[2,4]=C[2,2]+C[3,4]+r2r3r5=0+240+10×4×10=640→(M2)(M3M4)
k
=
4
→
C
[
2
,
4
]
=
C
[
2
,
3
]
+
C
[
4
,
4
]
+
r
2
r
4
r
5
=
240
+
0
+
10
×
6
×
10
=
840
→
(
M
2
M
3
)
(
M
4
)
k=4\rightarrow C[2,4]=C[2,3]+C[4,4]+r_2r_4r_5=240+0+10\times6\times10=840\rightarrow(M_2M_3)(M_4)
k=4→C[2,4]=C[2,3]+C[4,4]+r2r4r5=240+0+10×6×10=840→(M2M3)(M4)
这个例子最终的答案即是
C
[
1
,
5
]
=
348
C[1,5]=348
C[1,5]=348 次。
—————————————————————————————————
所有点对的最短路径问题
————————————
e
x
a
m
p
l
e
_
3
example\_3
example_3——————————————
设
G
=
(
V
,
E
)
G=(V,E)
G=(V,E) 是一个有向图,其中的每条边
(
i
,
j
)
(i,j)
(i,j) 都有一个非负的长度值
l
[
i
,
j
]
l[i,j]
l[i,j],此外,如果从顶点
i
i
i 到顶点
j
j
j 没有边,则人为地定义
l
[
i
,
j
]
=
∞
l[i,j]=\infty
l[i,j]=∞。所谓“所有点对的最短路径问题”,就是要找出从每个顶点到其它所有顶点的距离(从顶点
x
x
x 到顶点
y
y
y 的距离是指从
x
x
x 到
y
y
y 的最短路径的长度)。
现在,假设
V
=
{
1
,
2
,
…
,
n
}
V=\{1,2,\dots,n\}
V={1,2,…,n},
i
,
j
i,j
i,j 是
V
V
V 中两个不同的顶点,定义
d
i
,
j
k
d_{i,j}^k
di,jk 是从
i
i
i 到
j
j
j,并且不经过
{
k
+
1
,
k
+
2
,
…
,
n
}
\{k+1,k+2,\dots,n\}
{k+1,k+2,…,n} 中任何顶点的最短路径的长度。例如,
d
i
,
j
0
=
l
[
i
,
j
]
,
d
i
,
j
1
d_{i,j}^0=l[i,j],d_{i,j}^1
di,j0=l[i,j],di,j1 是从
i
i
i 到
j
j
j,除了可能经过顶点
1
1
1 以外,不经过任何其它顶点的最短路径,
d
i
,
j
2
d_{i,j}^2
di,j2 是
i
i
i 到
j
j
j,除了可能经过顶点
1
,
2
1,2
1,2 或者同时经过它们以外,不经过任何其它顶点的最短路径等。
因此,我们知道,
d
i
,
j
n
d_{i,j}^n
di,jn 是
i
i
i 到
j
j
j 的最短路径长度,也就是
i
i
i 到
j
j
j 的距离,那么就可以得到如下递推式:
d
i
,
j
k
=
{
l
[
i
,
j
]
i
f
k
=
0
min
{
d
i
,
j
k
−
1
,
d
i
,
k
k
−
1
+
d
k
.
j
k
−
1
}
i
f
1
⩽
k
⩽
n
d_{i,j}^k=\begin{cases}l[i,j]\hspace{1cm}if\space\space k=0\\\\\min\{d_{i,j}^{k-1},d_{i,k}^{k-1}+d_{k.j}^{k-1}\}\hspace{1cm}if\space\space1\leqslant k\leqslant n\end{cases}
di,jk=⎩⎪⎨⎪⎧l[i,j]if k=0min{di,jk−1,di,kk−1+dk.jk−1}if 1⩽k⩽n
根据动态规划算法的核心思路,我们需要自底向上地求解上述递推式。
F
l
o
y
d
Floyd
Floyd 提出,我们可以用
n
+
1
n+1
n+1 个
n
×
n
n\times n
n×n 维矩阵
D
0
,
D
1
,
…
,
D
n
D_0,D_1,\dots,D_n
D0,D1,…,Dn 来计算最短约束路径的长度。
开始时,如果
i
≠
j
i\neq j
i=j 并且
(
i
,
j
)
(i,j)
(i,j) 是
G
G
G 中的边,则设置
D
0
[
i
,
i
]
=
0
,
D
0
[
i
,
j
]
=
l
[
i
,
j
]
D_0[i,i]=0,D_0[i,j]=l[i,j]
D0[i,i]=0,D0[i,j]=l[i,j],否则令
D
0
[
i
,
j
]
=
∞
D_0[i,j]=\infty
D0[i,j]=∞。然后做
n
n
n 次迭代,使在第
k
k
k 次迭代后,
D
k
[
i
,
j
]
D_k[i,j]
Dk[i,j] 含有从顶点
i
i
i 到
j
j
j,且不经过编号大于
k
k
k 的任何顶点的最短路径的长度。这样就可以在第
k
k
k 次迭代中,用公式
D
k
[
i
,
j
]
=
min
{
D
k
−
1
[
i
,
j
]
,
D
k
−
1
[
i
,
k
]
+
D
k
−
1
[
k
,
j
]
}
D_k[i,j]=\min\{D_{k-1}[i,j],D_{k-1}[i,k]+D_{k-1}[k,j]\}
Dk[i,j]=min{Dk−1[i,j],Dk−1[i,k]+Dk−1[k,j]} 计算
D
k
[
i
,
j
]
D_k[i,j]
Dk[i,j]。下面我们根据一个实例来理解一下上述思路:
已知如下有向图:
根据定义一步一步计算,得到如下矩阵:
D
0
=
[
0
2
9
8
0
6
1
∞
6
]
D_0=\left[\begin{matrix}0&2&9\\\\8&0&6\\\\1&\infty&6\end{matrix}\right]\hspace{1cm}
D0=⎣⎢⎢⎢⎢⎡08120∞966⎦⎥⎥⎥⎥⎤
D
1
=
[
0
2
9
8
0
6
1
3
0
]
D_1=\left[\begin{matrix}0&2&9\\\\8&0&6\\\\1&3&0\end{matrix}\right]
D1=⎣⎢⎢⎢⎢⎡081203960⎦⎥⎥⎥⎥⎤
D
2
=
[
0
2
8
8
0
6
1
3
0
]
D_2=\left[\begin{matrix}0&2&8\\\\8&0&6\\\\1&3&0\end{matrix}\right]\hspace{1cm}
D2=⎣⎢⎢⎢⎢⎡081203860⎦⎥⎥⎥⎥⎤
D
3
=
[
0
2
8
7
0
6
1
3
0
]
D_3=\left[\begin{matrix}0&2&8\\\\7&0&6\\\\1&3&0\end{matrix}\right]
D3=⎣⎢⎢⎢⎢⎡071203860⎦⎥⎥⎥⎥⎤
最终答案就存储在矩阵
D
3
D_3
D3 之中。
根据上面的例子,我们发现在第
k
k
k 次迭代中,第
k
k
k 行和第
k
k
k 列都是不变的,因此可以仅用
D
D
D 矩阵的一个副本来进行计算,算法
f
l
o
y
d
floyd
floyd 如下:
- 输入: n × n n\times n n×n 维矩阵 l [ 1 ⋯ n , 1 ⋯ n ] l[1\cdots n,\space1\cdots n] l[1⋯n, 1⋯n],有向图 G = ( { 1 , 2 , … , n } , E ) G=(\{1,2,\dots,n\},E) G=({1,2,…,n},E) 中的边 ( i , j ) (i,j) (i,j) 的长度为 l [ i , j ] l[i,j] l[i,j]
- 输出:矩阵 D D D,使得 D [ i , j ] D[i,j] D[i,j] 等于 i i i 到 j j j 的距离
- 1. D ← l { \hspace{1cm}1.\space D\leftarrow l\space\space\{ 1. D←l {将输入矩阵 l l l 复制到 D } D\} D}
- 2. f o r k ← 1 t o n \hspace{1cm}2.\space for\space\space k\leftarrow1\space\space to\space\space n 2. for k←1 to n
- 3. f o r i ← 1 t o n \hspace{1cm}3.\space\hspace{1cm}for\space\space i\leftarrow1\space\space to\space\space n 3. for i←1 to n
- 4. f o r j ← 1 t o n \hspace{1cm}4.\space\hspace{2cm}for\space\space j\leftarrow1\space\space to\space\space n 4. for j←1 to n
- 5. D [ i , j ] = min { D [ i , j ] , D [ i , k ] + D [ k , j ] } \hspace{1cm}5.\space\hspace{3cm}D[i,j]=\min\{D[i,j],D[i,k]+D[k,j]\} 5. D[i,j]=min{D[i,j],D[i,k]+D[k,j]}
- 6. e n d f o r \hspace{1cm}6.\space\hspace{2cm}end\space for 6. end for
- 7. e n d f o r \hspace{1cm}7.\space\hspace{1cm}end\space for 7. end for
- 8. e n d f o r \hspace{1cm}8.\space end\space for 8. end for
显然,算法的时间复杂度为
Θ
(
n
3
)
\Theta(n^3)
Θ(n3),空间复杂度为
Θ
(
n
2
)
\Theta(n^2)
Θ(n2)。
—————————————————————————————————
0-1 背包问题
————————————
e
x
a
m
p
l
e
_
4
example\_4
example_4——————————————
问题定义:设
U
=
{
u
1
,
u
2
,
…
,
u
n
}
U=\{u_1,u_2,\dots,u_n\}
U={u1,u2,…,un} 是一个准备放入容量为
C
C
C 的背包中的
n
n
n 项物品的集合。对于
1
⩽
j
⩽
n
1\leqslant j\leqslant n
1⩽j⩽n,令
s
j
s_j
sj 和
v
j
v_j
vj 分别为第
j
j
j 项物品的体积和价值,这里假设上述
C
,
s
j
,
v
j
,
j
C,s_j,v_j,j
C,sj,vj,j 都是正整数。所谓 0-1 背包问题就是用
U
U
U 中的一些物品来装满背包,这些物品的总体积不超过
C
C
C,同时使它们的总价值最大。即给出有
n
n
n 项物品的
U
U
U,我们要找出一个子集合
S
⊆
U
S\subseteq U
S⊆U,使得
∑
u
i
∈
S
v
i
\sum_{u_i\in S}v_i
∑ui∈Svi 在约束条件
∑
u
i
∈
S
s
i
⩽
C
\sum_{u_i\in S}s_i\leqslant C
∑ui∈Ssi⩽C 下最大。(之所以称为 0-1 背包问题,是因为一个物品要么装入,要么不装入,这两种状态分别用 1 和 0 表示)
首先,我们需要导出递推公式,设
V
[
i
,
j
]
V[i,j]
V[i,j] 用来表示从前
i
i
i 项物品(
{
u
1
,
u
2
,
…
,
u
i
}
\{u_1,u_2,\dots,u_i\}
{u1,u2,…,ui})中取出来的装入体积为
j
j
j 的背包的最大价值。这里,
i
i
i 的范围是从
0
0
0 到
n
n
n,
j
j
j 的范围是从
0
0
0 到
C
C
C,因此最终答案即
V
[
n
,
C
]
V[n,C]
V[n,C]。显然,
V
[
0
,
j
]
V[0,j]
V[0,j] 对于所有
j
j
j 的值是
0
0
0,因为背包中什么也没有;
V
[
i
,
0
]
V[i,0]
V[i,0] 对于所有
i
i
i 的值也是
0
0
0,因为没有东西可以放到容量为
0
0
0 的背包。而当
i
>
0
,
j
>
0
i>0,j>0
i>0,j>0 时,有
- V [ i − 1 , j ] V[i-1,j] V[i−1,j]:仅用最优的方法取自 { u 1 , u 2 , … , u i − 1 } \{u_1,u_2,\dots,u_{i-1}\} {u1,u2,…,ui−1} 的物品去装入体积为 j j j 的背包所得的价值最大值
- V [ i − 1 , j − s i ] + v i V[i-1,j-s_i]+v_i V[i−1,j−si]+vi:用最优的方法取自 { u 1 , u 2 , … , u i − 1 } \{u_1,u_2,\dots,u_{i-1}\} {u1,u2,…,ui−1} 的物品去装入体积为 j − s i j-s_i j−si 的背包所得的价值最大值加上物品 u i u_i ui 的价值。这仅用于若 j ⩾ s i j\geqslant s_i j⩾si 以及它等于把物品 u i u_i ui 加到背包上的情况
- V [ i , j ] = max { V [ i − 1 , j ] , V [ i − 1 , j − s i ] + v i } V[i,j]=\max\{V[i-1,j],\space V[i-1,j-s_i]+v_i\} V[i,j]=max{V[i−1,j], V[i−1,j−si]+vi}
综上所述,得到
V
[
i
,
j
]
=
{
0
i
f
i
=
0
o
r
j
=
0
V
[
i
−
1
,
j
]
i
f
j
<
s
i
max
{
V
[
i
−
1
,
j
]
,
V
[
i
−
1
,
j
−
s
i
]
+
v
i
}
i
f
i
>
0
a
n
d
j
⩾
s
i
V[i,j]=\begin{cases}0\hspace{1cm}if\space\space i=0\space\space or\space\space j=0\\\\V[i-1,j]\hspace{1cm}if\space\space j<s_i\\\\\max\{V[i-1,j],\space V[i-1,j-s_i]+v_i\}\hspace{1cm}if\space\space i>0\space\space and\space\space j\geqslant s_i\end{cases}
V[i,j]=⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧0if i=0 or j=0V[i−1,j]if j<simax{V[i−1,j], V[i−1,j−si]+vi}if i>0 and j⩾si
根据动态规划的思路,我们自底向上进行计算,并且逐行将值存入
(
n
+
1
)
×
(
C
+
1
)
(n+1)\times(C+1)
(n+1)×(C+1) 的表,下面是算法
k
n
a
p
s
a
c
k
knapsack
knapsack:
- 输入:物品集合 U = { u 1 , u 2 , … , u n } U=\{u_1,u_2,\dots,u_n\} U={u1,u2,…,un},体积分别为 s 1 , s 2 , … , s n s_1,s_2,\dots,s_n s1,s2,…,sn,价值分别为 v 1 , v 2 , … , v n v_1,v_2,\dots,v_n v1,v2,…,vn,背包容量 C C C
- 输出: ∑ u i ∈ S v i \sum_{u_i\in S}v_i ∑ui∈Svi 的最大总价值,且满足 ∑ u i ∈ S s i ⩽ C , S ⊆ U \sum_{u_i\in S}s_i\leqslant C,\space\space S\subseteq U ∑ui∈Ssi⩽C, S⊆U
- 1. f o r i ← 0 t o n \hspace{1cm}1.\space for\space\space i\leftarrow0\space\space to\space\space n 1. for i←0 to n
- 2. V [ i , 0 ] ← 0 \hspace{1cm}2.\space\hspace{1cm}V[i,0]\leftarrow0 2. V[i,0]←0
- 3. e n d f o r \hspace{1cm}3.\space end\space for 3. end for
- 4. f o r j ← 0 t o C \hspace{1cm}4.\space for\space\space j\leftarrow0\space\space to\space\space C 4. for j←0 to C
- 5. V [ 0 , j ] ← 0 \hspace{1cm}5.\space\hspace{1cm}V[0,j]\leftarrow0 5. V[0,j]←0
- 6. e n d f o r \hspace{1cm}6.\space end\space for 6. end for
- 7. f o r i ← 1 t o n \hspace{1cm}7.\space for\space\space i\leftarrow1\space\space to\space\space n 7. for i←1 to n
- 8. f o r j ← 1 t o C \hspace{1cm}8.\space\hspace{1cm}for\space\space j\leftarrow1\space\space to\space\space C 8. for j←1 to C
- 9. V [ i , j ] ← V [ i − 1 , j ] \hspace{1cm}9.\space\hspace{2cm}V[i,j]\leftarrow V[i-1,j] 9. V[i,j]←V[i−1,j]
- 10. i f s i ⩽ j t h e n V [ i , j ] ← max { V [ i , j ] , V [ i − 1 , j − s i ] + v i } \hspace{0.85cm}10.\space\hspace{2cm}if\space\space s_i\leqslant j\space\space then\space\space V[i,j]\leftarrow\max\{V[i,j],\space V[i-1,j-s_i]+v_i\} 10. if si⩽j then V[i,j]←max{V[i,j], V[i−1,j−si]+vi}
- 11. e n d f o r \hspace{0.85cm}11.\space\hspace{1cm}end\space for 11. end for
- 12. e n d f o r \hspace{0.85cm}12.\space end\space for 12. end for
- 13. r e t u r n V [ n , C ] \hspace{0.85cm}13.\space return\space\space V[n,C] 13. return V[n,C]
显然,由于计算表的每一项需要
Θ
(
1
)
\Theta(1)
Θ(1) 时间,算法的时间复杂度恰好是表的大小
Θ
(
n
C
)
\Theta(nC)
Θ(nC)。0-1 背包问题的具体实例可参考我之前写过的一篇题解:蓝桥杯——算法训练——开心的金明(附0-1背包讲解)。
—————————————————————————————————