动态规划技术已经广泛应用于许多组合优化问题的算法设计中,它把求解过程变成一个多步判断的过程,每一步都对应于某个子问题. 算法细心地划分子问题的边界,从小的子问题开始,逐层向上求解. 通过子问题之间的依赖关系,有效利用前面已经得到的结果,最大限度减少重复工作,以提高算法效率.
我们先从一个上楼问题开始,我觉得这个公众号给出的引入问题比教材里给的例题好理解:
【例 1】一个楼梯有 10 级台阶,你从下往上走,每跨一步只能向上迈 1 级或者 2 级台阶,请问一共有多少种走法?
注意,千万不要陷入穷举的思想,跑偏了,规模一大就玩完
【解】
要想走到第 10 级台阶,要么是先走到第 9 级,然后再迈一步 1 级台阶上去,要么是先走到第 8 级,然后一次迈 2 级台阶上去.
这样的话,走到 10 级台阶的走法数,就等于走到 9 级台阶的走法数,加上走到 8 级台阶的走法数.
假设走到第
x
x
x 级台阶的走法数我们定义为
F
(
x
)
F(x)
F(x),那么就有:
F
(
x
)
=
F
(
x
−
1
)
+
F
(
x
−
2
)
F(x)=F(x-1)+F(x-2)
F(x)=F(x−1)+F(x−2)
既然有了递推式,那总得有边界条件吧,显然, F ( 1 ) = 1 F(1)=1 F(1)=1, F ( 2 ) = 2 F(2)=2 F(2)=2,这样,就可以据此编写代码了.
由于这道题是一道经典的动态规划题,所以我们以这道题为例子来定义动态规划的三要素,在本题中:
-
F ( x − 1 ) F(x-1) F(x−1) 和 F ( x − 2 ) F(x-2) F(x−2) 被称为 F ( x ) F(x) F(x) 的最优子结构
一个最优决策序列的任何子序列本身一定是相对于子序列的初始和结束状态的最优的决策序列.
-
F ( x ) = F ( x − 1 ) + F ( x − 2 ) F(x)=F(x-1)+F(x-2) F(x)=F(x−1)+F(x−2) 称为状态转移方程
-
F ( 1 ) = 1 F(1)=1 F(1)=1, F ( 2 ) = 2 F(2)=2 F(2)=2 是问题的边界
之后做动态规划问题,只要找好这三个要素就好了.
下面介绍截个动态规划的典型应用:
0-1背包问题
【例 2】有一个背包,可以装载重量为 5 kg 的物品,有 4 个物品,他们的重量和价值如下:
那么请问,在不得超过背包的承重的情况下,将哪些物品放入背包,可以使得总价值最大?
【解】
我们先来几个定义方便描述,我们将 4 个物品的重量和价值分别表示为: W 1 W1 W1, W 2 W2 W2, W 3 W3 W3, W 4 W4 W4, V 1 V1 V1, V 2 V2 V2, V 3 V3 V3, V 4 V4 V4.
假如我们用 F ( W , i ) F(W,i) F(W,i) 表示 用载重为 W W W 的背包,装前 i i i 件物品的最大价值,那本体其实就是用载重为 5 kg 的背包,装前 4 件物品的最大价值,其实就是求解 F ( 5 , 4 ) F(5,4) F(5,4).
如何找状态转移方程?其实与上楼梯的问题类似,针对物品 4,我们有装或不装两种选择,可以得到如下式子:
F
(
5
,
4
)
=
max
{
F
(
1
,
3
)
+
6
,
F
(
5
,
3
)
}
\displaystyle F(5,4)=\max\{F(1,3)+6, F(5,3)\}
F(5,4)=max{F(1,3)+6,F(5,3)}
如果装入物品 4,那么只需求载重 1 kg 的背包,如何装前 3 件物品是的总价值最大;如果不装入物品 4,那么只需求载重 5 kg 的背包,如何装前 3 件物品是的总价值最大.
两种情况取最大的那个就是最优结果.
现在状态转移方程出来了,此时我们画个表格.
我们的目标就是要计算右下角那个值,即背包载重 W = 5 W = 5 W=5 时,选择前 4 件物品放入背包的最大价值 F ( 5 , 4 ) F(5,4) F(5,4),那也就是说只要知道 F ( 1 , 3 ) F(1,3) F(1,3) 和 F ( 5 , 3 ) F(5,3) F(5,3) 的值就可以了.
再看看 F ( 1 , 3 ) F(1,3) F(1,3) 怎么计算,显然 1 kg 装不下 3 件物品,故有 F ( 1 , 3 ) = F ( 1 , 2 ) = F ( 1 , 1 ) = 3 F(1,3)=F(1,2)=F(1,1)=3 F(1,3)=F(1,2)=F(1,1)=3
这样我们就找到了一个边界值. 求出所有边界值如下:
接下来,就依次把表格的所有项都填出来,自然就可以算出 F ( 5 , 4 ) F(5,4) F(5,4):
最长公共子序列 LCS
【例 3】求 X = < A , B , C , B , D , A , B > , Y = < B , D , C , A , B , A > X=\lt A,B,C,B,D,A,B\gt,Y=\lt B,D,C,A,B,A\gt X=<A,B,C,B,D,A,B>,Y=<B,D,C,A,B,A> 的最长公共子序列 Z Z Z
【解】
这题怎么找最优子结构呢?首先,我们先将问题重新定义一下:
X
=
<
A
,
B
,
C
,
B
,
D
,
A
,
B
>
=
<
x
1
,
x
2
,
x
3
,
x
4
,
x
5
,
x
6
,
x
7
>
Y
=
<
B
,
D
,
C
,
A
,
B
,
A
>
=
<
y
1
,
y
2
,
y
3
,
y
4
,
y
5
,
y
6
>
Z
=
<
z
1
,
z
2
,
⋯
,
z
k
>
,
k
表示最长公共子序列的长度
\begin{aligned} \displaystyle X &=\lt A,B,C,B,D,A,B\gt = \lt x_1,x_2,x_3,x_4,x_5,x_6,x_7\gt \\ \\ \displaystyle Y &=\lt B,D,C,A,B,A\gt = \lt y_1,y_2,y_3,y_4,y_5,y_6\gt \\ \\ \displaystyle Z &= \lt z_1,z_2,\cdots,z_k\gt,k \,\text{表示最长公共子序列的长度} \end{aligned}
XYZ=<A,B,C,B,D,A,B>=<x1,x2,x3,x4,x5,x6,x7>=<B,D,C,A,B,A>=<y1,y2,y3,y4,y5,y6>=<z1,z2,⋯,zk>,k表示最长公共子序列的长度
老办法,假如我们用 F ( i , j ) F(i,j) F(i,j) 表示 X X X 序列的前 i i i 个子序列和 Y Y Y 序列的前 j j j 个子序列的最长公共子序列的长度.
为啥是长度而不是序列呢?因为状态转移方程是量化的!要转为数学问题来解决. 我们只需在算法过程中记录结点编号即可得到最长公共子序列. 当然,结果不唯一.
那么该问题其实是求解
F
(
7
,
6
)
F(7,6)
F(7,6). 针对最后一对结点
x
7
x_7
x7 和
y
6
y_6
y6,有相等和不等两种情况,可以得到如下递推式:
F
(
7
,
6
)
=
{
F
(
6
,
5
)
+
1
x
7
=
y
6
max
{
F
(
6
,
6
)
,
F
(
7
,
5
)
}
x
7
≠
y
6
F(7,6)=\begin{cases} F(6,5)+1 \quad \quad \quad \quad \quad \quad \ \ x_7=y_6 \\ \\ \max\{F(6,6),F(7,5)\}\quad\quad x_7\ne y_6 \end{cases}
F(7,6)=⎩⎪⎨⎪⎧F(6,5)+1 x7=y6max{F(6,6),F(7,5)}x7=y6
推广到一般形式,有:
F
(
i
,
j
)
=
{
F
(
i
−
1
,
j
−
1
)
+
1
x
i
=
y
j
max
{
F
(
i
−
1
,
j
)
,
F
(
i
,
j
−
1
)
}
x
i
≠
y
j
F(i,j)=\begin{cases} F(i-1,j-1)+1 \quad \quad \quad \quad \quad \quad \ \ x_i=y_j \\ \\ \max\{F(i-1,j),F(i,j-1)\}\quad\quad x_i\ne y_j \end{cases}
F(i,j)=⎩⎪⎨⎪⎧F(i−1,j−1)+1 xi=yjmax{F(i−1,j),F(i,j−1)}xi=yj
现在,状态转移方程找到了,那么边界条件是什么呢?显然是 i = 1 i=1 i=1 或 j = 1 j=1 j=1 时候的最长公共子序列,如图:
接下来,就依次把表格的所有项都填出来,自然就可以算出 F ( 7 , 6 ) : F(7,6): F(7,6):
最大子段和
【例 4】求序列 [4, -3, 5, -2, -1, 2, 6, -2] 的最大子段和
【解】
首先还是问题定义:
L
=
[
4
,
−
3
,
5
,
−
2
,
−
1
,
2
,
6
,
−
2
]
=
L
1
∼
L
8
L = [4, -3, 5, -2, -1, 2, 6, -2] = L_1 \sim L_8
L=[4,−3,5,−2,−1,2,6,−2]=L1∼L8
假如我们用
F
(
n
)
F(n)
F(n) 表示序列
L
L
L 前
n
n
n 个子序列的最大子段和,那么该问题其实就是求解
F
(
8
)
F(8)
F(8),针对第
L
8
L_8
L8,可以有如下递推式:
F
(
8
)
=
max
{
F
(
7
)
,
F
(
7
)
+
L
8
}
F(8)=\max \{ F(7),\ F(7)+L_8 \}
F(8)=max{F(7), F(7)+L8}
等等,好像有哪里不对!
F
(
7
)
F(7)
F(7) 不一定是最后几个数的和,和
L
8
L_8
L8 不一定挨着,这题不像之前可以无脑写递推方程了.
那该怎么办呢?
既然发现上面的弊端,那么,我们就添加个限制条件,
F
(
n
)
F(n)
F(n) 表示序列
L
L
L 前
n
n
n 个子序列的以
L
n
L_n
Ln 结尾的最大子段和,此时递推式应该变为这样:
F
(
8
)
=
max
{
F
(
7
)
+
L
8
,
L
8
}
F(8)=\max \{ F(7)+L_8,\ L_8 \}
F(8)=max{F(7)+L8, L8}
为什么?不会吧,不会吧,看到现在都没发现之前的例题都是最终结论倒推一步来找状态转移方程的吧~
然后,根据修改后的定义,递推式只能是这样,因为必须以 L n L_n Ln 结尾
但是, F ( n ) F(n) F(n) 就不能代表我们要求解的问题了,那么,现在问题的描述应该是什么呢?
当然不能凭空想,看看 F ( n ) F(n) F(n),怎么样将它和问题挂钩.
我们之所以更改 F ( n ) F(n) F(n) 的定义,就是因为子段和的位置问题,那么,我们有没有办法利用修改过的定义来消除这种位置问题?
当然可以!序列的最大子段和必然以某一个元素结尾,那么我们可以定义:
M
S
S
(
n
)
=
max
1
≤
k
≤
n
F
(
k
)
MSS(n) = \displaystyle\max_{1 \le k \le n}F(k)
MSS(n)=1≤k≤nmaxF(k)
这样,我们就把问题转化成一个动态规划问题,和一个简单排序问题.
现在,解题思路很明确,但是如果要写代码,就不像之前的例题立体那么好写了.
死板点的方法就是 F ( 1 ) ∼ F ( n ) F(1)\sim F(n) F(1)∼F(n) 先全部求一遍再比大小,但是规模 n n n 变大会导致时间复杂度还是不可接受,最好求完一遍结果也能出来!
这里由于要遍历,所以肯定是 f o r for for 循环,因此可以添加中间变量同时记录最大的子段和以及其末尾位置,算法如下所示:
第 4 行 b > 0 b \gt 0 b>0 其实是 b + L [ i ] > L [ i ] b + L\left[ \ i \ \right] > L\left[ \ i \ \right] b+L[ i ]>L[ i ]