动态规划问题
动态规划是一种求最优解的算法思想,它同分治法类似,都是讲问题划分为若干个独立的子问题,解决完子问题之后就能得到该问题的解。
斐波拉契数列
斐波拉契是一个常见的数列,其中的每一个数都是又它的前两个数相加的出来的,其公式如下:
f
(
x
)
=
{
1
,
x
=
0
&
x
=
1
f
(
x
−
1
)
+
f
(
x
−
2
)
,
x
>
=
2
f(x) = \begin{cases} 1, & x=0\&x=1 \\ f(x-1)+f(x-2), & x>=2 \end{cases}
f(x)={1,f(x−1)+f(x−2),x=0&x=1x>=2
我们很容易就能写出对应的递归函数:
def fibonacci(x):
if x == 1 or x == 2:
return 1
return fibonacci(x - 1) + fibonacci(x - 2)
这是一个典型的分治法的递归式。但是,这存在一个问题,那就是当数字足够大时,会进行大量的重复计算:
分治法是不会记录子问题的结果的,它只会频繁地计算子问题。而动态规划会考虑公共子问题,也就是那些被重复计算的部分。动态规划会将这些问题记录下来,避免每次遇到这些重复的子问题时重新反复的计算。
在这里,我们定义一个数组,用于存储其计算的结果:
def fibonacci2(x):
temp = [0 for i in range(x)]
temp[0], temp[1] = 1, 1
def run(i):
if temp[i] == 0:
temp[i] = run(i - 1) + run(i - 2)
return temp[i]
return run(x-1)
矩阵链乘法问题
由于 n n n个矩阵进行相乘 A 1 ∗ A 2 ∗ . . . ∗ A n A_1*A_2*...*A_n A1∗A2∗...∗An。我们知道,再两个矩阵相乘 A n ∗ m ∗ B m ∗ k A_{n*m}*B_{m*k} An∗m∗Bm∗k时,根据矩阵乘法的运算规则,其会进行 n ∗ m ∗ k n*m*k n∗m∗k次数乘运算:
2 ∗ 3 2*3 2∗3 和 3 ∗ 2 3*2 3∗2的矩阵相乘运算了 2 ∗ 3 ∗ 2 2*3*2 2∗3∗2数乘运算:
( a b c d e f ) ∗ ( a b c d e f ) = ( a ∗ a + b ∗ c + c ∗ e a ∗ b + b ∗ d + c ∗ f d ∗ a + e ∗ c + f ∗ e d ∗ b + e ∗ d + f ∗ f ) \begin{pmatrix} a & b & c \\ d & e & f \\ \end{pmatrix} * \begin{pmatrix} a & b \\ c & d \\ e & f \\ \end{pmatrix} = \begin{pmatrix} a*a+b*c+c*e & a*b+b*d+c*f \\ d*a+e*c+f*e & d*b+e*d+f*f \\ \end{pmatrix} (adbecf)∗ acebdf =(a∗a+b∗c+c∗ed∗a+e∗c+f∗ea∗b+b∗d+c∗fd∗b+e∗d+f∗f)
当三个矩阵( n ∗ m , m ∗ k , k ∗ s n*m, m*k, k*s n∗m,m∗k,k∗s)相乘的时候,在先进行前两个矩阵运算时得到结果的运算次数为 n ∗ m ∗ k + n ∗ k ∗ s n*m*k+n*k*s n∗m∗k+n∗k∗s, 而先进行后两个举证运算时得到结果的运算次数为 m ∗ k ∗ s + n ∗ m ∗ s m*k*s+n*m*s m∗k∗s+n∗m∗s。可以看出,矩阵乘法运用结合律后虽然得到的结果相同,但是我们的计算成本不同。
这样,我们可以运用结合律来减少运算次数来得出 n n n个矩阵相称的结果。那么,这个最少的运算次数是多少?
在这里,我们将第
n
n
n个到第
m
m
m个矩阵的运算结果标记为
S
n
m
S_{nm}
Snm,第
i
i
i个矩阵的行数为
p
i
p_i
pi,列为
p
i
−
1
p_{i-1}
pi−1,我们
S
n
m
S_{nm}
Snm再
A
k
A_{k}
Ak处进行拆分,得到
S
n
m
=
S
n
k
∗
S
k
m
S_{nm}=S_{nk}*S_{km}
Snm=Snk∗Skm,这样,我们计算
S
n
m
S_{nm}
Snm的成本就是计算
S
n
k
S_{nk}
Snk和
S
k
m
S_{km}
Skm与这两个结果矩阵相乘的和,我们将
m
m
m到
n
n
n的计算成本记为
m
n
m
m_{nm}
mnm:
m
n
m
=
{
0
,
m
=
n
min
n
<
=
k
<
n
(
m
n
k
+
m
k
m
+
p
m
−
1
∗
p
k
∗
p
n
)
,
m
<
n
m_{nm} = \begin{cases} 0, & m=n\\ \min_{n<=k<n}({m_{nk}+m_{km}+p_{m-1}*p_{k}*p_{n}}), & m<n \end{cases}
mnm={0,minn<=k<n(mnk+mkm+pm−1∗pk∗pn),m=nm<n
可以得出,我们在冲第
n
n
n个到第
m
m
m个矩阵组成的矩阵链中,用任意的第
k
k
k个矩阵将其拆分成两个不同的字矩阵链并得到其结果,再对这些结果进行比较,便可以得到计算开销最小的那一个。根据该公式,我们也能很容易写出其对应的递归代码。
动态规划基础
最优子结构
利用动态规划来解决问题,首先第一步就是描述其最优子结构。在斐波拉契中,我们求第
n
n
n个数可以通过第
n
−
1
n-1
n−1和第
n
−
2
n-2
n−2个数来求解,而在矩阵链相乘的问题中,也可以将这个矩阵链拆分成两个子矩阵链。这些问题中,我们通过子问题的最优解来构造其最优解。动态规划问题是一种自底向上的方法,也就是说在解决一个问题时,首先要解决它的子问题的最优解。我们可以发现,这些问题的解都可以看作是做一个选择,比如矩阵链乘法问题就是选择合适的矩阵对其进行拆分,并且在这些问题中,其最优解的子问题也一堆是最优解。
在下图中,我们可以求得A-C-D
是A
到D
的最短路径,而且它的两个子路径A-C
和C-D
也是最短路径,也就是说,这是他的最优子结构。
但是,若是换成求最长简单路径就不一样了。我们同样可以看出A-C-D
是A
到D
的最长简单路径,但是A-C
和C-D
却不是其最长简单路径,其最长简单路径为A-B-D-C
和C-A-B-D
。
为什么这两个问题会有如此不同呢?这是因为最短路径问题的子问题是独立的,而最长简单路径问题的子问题不是独立的。
子问题独立是指一个子问题的解不会影响到另外的子问题的解。在最短路径中,A-C
和C-D
这两个最有子问题中间没有重复访问的节点,彼此是相互独立的。而在最长简单路径问题中,A-B-D-C
和C-A-B-D
这两条路径又重复访问的节点,这样合并这两条路径时得到的就不是一条简单路径,所以这两个子问题不是独立的。
重叠子问题
当一个递归算法在不断的调用同一个问题时,我们就说它具有重叠子问题。这也是动态规划和分治法不同的地方。前面提到分治法只是会将问题分解成子问题再计算,其每一步都产生全新的子问题,而动态规划则是利用这些重叠的子问题,这些重叠子问题只会计算一遍,然后存储起来,等到又一次用到的时候就直接的出结果,比如在斐波拉契问题中,我们就利用列表存储了计算出来的结果,在下次使用的时候直接查表而不消耗额外的时间。