#实验三 动态规划算法
一.实验目的
掌握动态规划算法的设计与分析步骤以及算法的具体实现。
二.实验要求
实验时间:2学时,对于给定的问题描述,生成实验报告。
三.实验内容
问题描述:给定序列$X=<x_{1}, x_{2}, … , x_{m}>$, $Y=<y_{1}, y_{2}, … , y{n}>$ 求 X 和 Y 的最长公共子序列及其长度。
- 使用蛮力算法解决问题,给出分析过程。
- 使用动态规划算法解决问题,给出分析过程。
- 给出算法的具体实现过程(源码及其详细注释)。
- 给出算法的运行结果。针对不同的输入,给出两种不同算法时间上的差异。
蛮力算法
思路
找出一个串的所有子序列,然后依次与另一个串比较,如果该子序列的排列符合另一个串的排列,则是一个公共子序列。记录子序列的长度,找出最大长度即可。
不难看出,此算法的核心在于找出一个串的所有子序列。
怎么找呢。
我们能够联想到,对于一个物品,选择和不选择,我们都能用0、1来表示。
对于一个序列中的每一个位置,我们也能通过选择与不选择的两种状态。
X串长m。则X有$2^m$个子序列。(因为每个位置可以有两种状态)
我们可以将m转化为二进制,每个位次分别将其与X
串的各个位置对应。
复杂度
共有$2^m$个子序列,时间复杂度$O(2^m)$。
每个子序列都需要与Y
比较,Y
长度为$n$,时间复杂度$O(n)$.
总的时间复杂度$O(n2^m)$.
DP
解决DP问题,最重要的是要找出子问题。所以从这里开始。
子问题
我们假设当前$Z=<Z_{1}, Z_{2}, … , Z_{k}>$已经是LCS,可能有$x_{m} = y_{n}$和$x_{m} \ne y_{n}$:
如果xm = yn
- 则$zk = xm = yn$ 且
Zk-1
是Xm-1
和Yn-1
的一个LCS
.
如果xm != yn
- 且有 $zk \ne xm$,则
Z
是Xm-1
和Y
的一个LCS
. - 且有$zk != yn$,则
Z
是X
和Yn-1
的一个LCS
.
状态转移方程
$$ L[i][j] = \begin{cases} 0 & i = 0 \space or \space j = 0
\\max{L[i-1][j],L[i][j-1]} & x_i\ne y_j,i>0,j>0
\ L[i-1][j-1]+1 & x_i = y_j,i>0,j>0
\end{cases}
$$
因此,我们只需要从c[0][0]
开始填表,填到c[m][n]
,所得到的c[m-1][n-1]
就是LCS_length
.
回溯
若想得到LCS
,回溯一次b数组
,从最后一个位置开始往前遍历:
- 如果箭头是
↖
,则代表这个字符是LCS
的一员,存下来后i-- , j--
- 如果箭头是
←
,则代表这个字符不是LCS
的一员,j--
- 如果箭头是
↑
,也代表这个字符不是LCS
的一员,i--
- 如此直到$i = 0$或者$j = 0$时停止,最后存下来的字符就是所有的
LCS
字符
序列 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
X | A | B | C | B | D | A | B |
Y | B | D | C | A | B | A | \0 |
Target | B | C | B | A |
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
1 | B[1,1]= ↑ | B[1,2]= ↑ | B[1,3]= ↑ | B[1,4]=↖ | B[1,5]= ← | B[1,6]=↖ |
2 | B[2,1]=↖ | B[2,2]= ← | B[2,3]= ← | B[2,4]= ↑ | B[2,5]=↖ | B[2,6]= ← |
3 | B[3,1]= ↑ | B[3,2]= ↑ | B[3,3]=↖ | B[3,4]= ← | B[3,5]= ↑ | B[3,6]= ↑ |
4 | B[4,1]= ↖ | B[4,2]= ↑ | B[4,3]= ↑ | B[4,4]= ↑ | B[4,5]=↖ | B[4,6]= ← |
5 | B[5,1]= ↑ | B[5,2]=↖ | B[5,3]= ↑ | B[5,4]= ↑ | B[5,5]= ↑ | B[5,6]= ↑ |
6 | B[6,1]= ↑ | B[6,2]= ↑ | B[6,3]= ↑ | B[6,4]=↖ | B[6,5]= ↑ | B[6,6]=↖ |
7 | B[7,1]=↖ | B[7,2]= ↑ | B[7,3]= ↑ | B[7,4]= ↑ | B[7,5]= ↖ | B[7,6]= ↑ |
复杂度
该算法无重复地填写了一张(如果需要具体的子序列内容,则是两张)二维表格,每个格子填写时都仅进行了常数次比较。时间复杂度 $O(mn)$.
回溯时,在任何情况下,都需要从[m][n]
回溯到[1][1]
,即回溯时间复杂度为$O(m+n)$.
所以DP算法的时间复杂度为
$$O(mn)$$
代码
1 |
|
结果
总结
蛮力算法改进
可以看到,蛮力算法的时间复杂度集中在求X
,即程序中S1
的子序列。若S1
足够短,则所消耗的时间会大大降低。
通过比较S1
与S2
的串长,转而求取较短的串的子序列,可以大幅缩短某些情况下的时间消耗。
S1
过长的情况
交换S1
与S2
DP改进
对于仅需要求取LCS_length
的问题。由于填写第i
行时,仅仅需要表格的第i
行和第i-1
行:利用滚动数组,可以优化空间。同时让更短的串作为列,仅需$O(min{m,n})$的空间复杂度。
体会
一般来说,蛮力算法是最容易想到的算法。但在此问题中,对于求出一个串的所有子序列这个问题仍然十分困难。我也是参考了网络上(overview)的方法和同学的建议才想出个所以然。
但要能熟悉了DP的解题思路,明白“找出子问题、构建方程、回溯求解”这三个部分,反而会更加地便于解决问题。