动态规划求解最长公共子序列

前言

推出一个新系列,《看图轻松理解数据结构和算法》,主要使用图片来描述常见的数据结构和算法,轻松阅读并理解掌握。本系列包括各类堆、各类队列、各类列表、各类树、各类图、各类排序等等几十篇的样子。mysql

最长公共子序列

最长公共子序列,英文为Longest Common Subsequence,缩写LCS。一个序列,若是是某两个或多个已知序列的最长子序列,则称为最长公共子序列。算法

另外,要注意的是最长公共子序列与最长公共子串不同,下面看一个例子就明白。sql

有序列S1和S2,其中S1=hello,S2=hero。那么最长公共子序列为heo,而最长公共子串为he。能够看到区别就在于一个容许不连续,一个要求必须连续,而共同特色就是都要保持顺序性。缓存

暴力穷举法

暴力穷举法是最简单粗暴且直观的解决方法,既然是暴力了那效率确定是最差。有X_m=<x_1,x_2,…,x_m>Y_n=<y_1,y_2,…,y_n>两个序列,穷举过程首先要枚举全部可能的子序列,对于序列X,它的子序列数量达到2^m,所以这部分的时间复杂度达到O(2^m)。而每一个子序列去匹配序列Y的时间复杂度为O(n),因此整个过程的时间复杂度为O(n*2^m)。也就是说暴力穷举法的时间复杂度达到指数级,而实际中序列长度可能较长,这时几乎没法使用该方法。网络

子序列的数量为什么是2^m?某个序列的全部子序列能够当作是从某序列中移除若干个(0到m个)元素后组成的序列,好比ABC,移除0个元素时为{ABC},移除1个元素时为{BC,AC,AB},移除2个元素时为{C,B,A},移除3个元素时为空。数据结构

暴力穷举大体步骤:并发

  1. 对于序列X,枚举全部子序列;
  2. 对第1步中每一个子序列匹配序列Y,记录匹配上的最长子序列;

动态规划

鉴于暴力穷举法的时间复杂度太大,须要另一种方法解决该问题,动态规划。通常在能用动态规划解决的问题须要符合三个特征:最优子结构、重叠子问题和无后效性。刚恰好,最长公共子序列问题符合动态规划特征,下面对该问题具体分析。机器学习

最优子结构

假设有X_m=<x_1,x_2,…,x_m>Y_n=<y_1,y_2,…,y_n>两个序列,记X、Y两个序列对应的最长公共子序列为LCS(X_m,Y_n),肯定LCS(X_m,Y_n)的过程就是一个最优化问题。为了分析最优子结构,咱们须要从序列X与序列Y的最后一个元素开始。分两种状况:数据结构和算法

  • 若是x_m=y_n,即序列X与序列Y两个序列的最后一个元素相同,说明该元素必定是公共子序列的最后一个元素,此时原问题的状态转换公式为LCS(X_m,Y_n) =LCS(X_{m-1},Y_{n-1}) +X_m。能够看到这种状况下,原问题已经成功分解成子问题,并且每一个阶段的最优解均可以经过子问题的最优解获得,符合最优子结构。学习

  • 若是x_m \neq y_n,即序列X与序列Y两个序列的最后一个元素不相同,此时须要考虑两种状况:

    1. 假如x_m不是最长公共子序列的最后一个元素,则问题的状态转换公式为LCS(X_m,Y_n) =LCS(X_{m-1},Y_{n}),即从X_m=<x_1,x_2,…,x_{m-1}>Y_n=<y_1,y_2,…,y_n>两个序列中找。
    2. 假如y_n不是最长公共子序列的最后一个元素,则问题的状态转换公式为LCS(X_m,Y_n) =LCS(X_{m},Y_{n-1}),即从X_m=<x_1,x_2,…,x_m>Y_n=<y_1,y_2,…,y_{n-1}>两个序列中找。

以上,成功将原问题分解成子问题,并且子问题的最优解最终组成整个问题的最优解,也就是说该问题具有最优子结构性质。

重叠子问题

通过以上分析,咱们将原问题分解成三个子问题:

  1. LCS(X_m,Y_n) =LCS(X_{m-1},Y_{n-1}) +X_m
  2. LCS(X_m,Y_n) =LCS(X_{m-1},Y_{n})
  3. LCS(X_m,Y_n) =LCS(X_{m},Y_{n-1})

从中能够看出来子问题是存在重叠的,好比对于LCS(X_{m-1},Y_{n}),当序列X_{m-1}与序列Y_{n}的最后一个元素不相同时,子问题会继续分解成LCS(X_{m-2},Y_{n-1})LCS(X_{m-1},Y_{n-1}),也就与前面的子问题LCS(X_m,Y_n) =LCS(X_{m-1},Y_{n-1}) +X_m中的LCS(X_{m-1},Y_{n-1})重叠了。

因此,原问题具有重叠子问题性质。

无后效性

从前面子问题的转换公式能够看出,某阶段的子问题肯定后再也不受后面决策的影响,即后面过程不会影响前面已经肯定的状态。反过来,也能够认为某阶段的子问题最优解由以前某些状态获得,而不用管以前的状态是如何获得的。

递归公式

全部问题都已经分析完毕,接下去定义递归公式,将全部状态及状态转移用递归公式描述清楚。

c[i,j]LCS(X_i,Y_j)的长度,其中i = 0,1,2,...mj=0,1,2,...n

c[i,j]= \left\{\begin{matrix} 
0,&  &if \quad i=0 \ or j=0\\
c[i-1,j-1]+1, & &if \quad i,j>0 \ and \ x_i=y_j \\
max(c[i,j-1],c[i-1,j])& &if \quad i,j>0 \ and \ x_i≠y_j
\end{matrix}\right.

动态规划实现方式

动态规划的实现方式有两种,即自顶向下(Top-down)与自底向上(Bottom-up)。

自顶向下方式:这种方式直接使用递归公式计算获得结果,问题的解可使用子问题的解递归地表示。另外,对于重叠的子问题能够将其记忆化,即保存在缓存表中,每次解决子问题以前先查缓存表,若是子问题已经解决,则咱们能够直接使用它。对于还未解决过的子问题,咱们先解决子问题,再把子问题的解存到缓存表中。

自底向上方式:相对于自顶向下,咱们能够反向找到另一种方式,以自底向上的方式从新构造问题。咱们不直接解决原问题,而是先尝试解决子问题,而后经过子问题解决更大的子问题,不断向着更大问题迭代从而解决最终的问题。

文章太长,自顶向下方式先不发出来。

自底向上方式

在实际工程中自底向上方式可使用一个二维表格来记录最长公共子序列的求解过程。

如今有序列X=HELLO,序列Y=HERO,用动态规划自底向上方式来求解它们的最长公共子序列。最开始时初始化整张表格,注意表格的两个维度都比各自序列长度大一维,多出的一维用于保存初始状态,初始状态都为0,在计算过程当中要用到这些初始状态。

image

构建子问题表格

从序列X的第一个元素开始,与序列Y的第一个元素对比,由于H=H,因此LCS(1,1)=LCS(0,0)+1=1。

image

接着H!=E,因而LCS(1,2)=max(LCS(1, 1), LCS(0, 2)),即LCS(1,2)=LCS(1,1)=1。

image

image

接着H!=R,因而LCS(1,3)=max(LCS(1, 2), LCS(0, 3)),即LCS(1,3)=LCS(1,2)=1。

image

image

接着H!=O,因而LCS(1,4)=max(LCS(1, 3), LCS(0, 4)),即LCS(1,4)=LCS(1,3)=1。

image

image

一样地,对于序列X的第二个元素也分别与序列Y的每一个元素对比,并将结果填入对应表格中,序列X的第二个元素对比完的结果以下。

image

序列X的第三个元素对比完的结果以下。

image

序列X的第四个元素对比完的结果以下。

image

序列X的第五个元素对比完,也就是最终的结果以下。

image

因此能够看到LCS(5,4)=3,也就是说序列X和序列Y的最长公共子序列的长度为3。

同时也能够看到,经过动态规划自底向上方法咱们只须要构建一张表格就能够获得最终的结果,而构建表格的时间复杂度为O(m*n),时间复杂度大大下降。

构建最长公共子序列

有时候获得最长公共子序列的长度还不能知足咱们的要求,咱们想进一步获得长公共子序列,这时就须要依据已经构建好的表格反推回去,最终获得结果。

也就是说先判断两序列的指定元素是否相同,若是相同则斜着往回走一格,但若是不相同能够则往左或往上走一格,根据值的大小决定往左仍是往上,值一样大的话则往左往上均可以。

接着上面的例子,通过前面过程后,表格已经构建成功,而且获得了最长公共子序列的长度。接下去咱们来获取最长公共子序列。从最后一格开始,

image

由于O=O,因此O属于最长公共子序列的元素,将其输出,接着斜着往回走一格。

image

由于L!=R,因此二者都不属于最长公共子序列的元素,并且往左往上的值都相等,可任意选择,这里选择往上走一格。

image

继续,由于L!=R,因此二者都不属于最长公共子序列的元素,并且往左往上的值都相等,可任意选择,这里选择往上走一格。

image

继续,由于E!=R,因此二者都不属于最长公共子序列的元素,其中左边的值大于上边的值,选择往左走一格。

image

继续,由于E=E,因此E属于最长公共子序列的元素,将其输出,接着斜着往回走一格。

image

继续,由于H=H,因此H属于最长公共子序列的元素,将其输出,此时已经走到尽头,如今全部输出的便是最长公共子序列,即HEO。而构建最长公共子序列的时间复杂度为O(m+n)。

image

-------------推荐阅读------------

个人开源项目汇总(机器&深度学习、NLP、网络IO、AIML、mysql协议、chatbot)

为何写《Tomcat内核设计剖析》

2018汇总数据结构算法篇

2018汇总机器学习篇

2018汇总Java深度篇

2018汇总天然语言处理篇

2018汇总深度学习篇

2018汇总JDK源码篇

2018汇总Java并发核心篇

2018汇总读书篇


跟我交流,向我提问:

欢迎关注:

相关文章
相关标签/搜索