[CLRS][CH 15.4] 最长公共子序列

---恢复内容开始---

摘要

介绍了最长公共子序列的概念及解题思路。

子序列概念

子序列:一个给定序列的子序列就是该给定序列中,去掉零个或多个元素。一般来说,给定一个序列 X = <x1, x2, ..., xm>,另一个序列 Z = <z1, z2, ..., zk> 如果存在X的一个严格递增下标序列<i1, i2, ..., ik>,使得所有的j = 1, 2, ..., k,有xij = zj,则Z是X的一个子序列。例如,Z = <B, C, D, B> 是 X = <A, B, C, B, D, A, B> 的一个子序列,相应下标序列为<2, 3, 5, 7>。

公共子序列:给定两个序列X和Z,如果Z既是X的子序列又是Y的子序列,则称Z为X和Y的公共子序列。

最长公共子序列(Longest Common Substring, LCS):就是最长的那个公共子序列了。

在最长子序列问题中,给定两个序列 X = <x1, x2, ..., xm> 和 Y = <y1, y2, ..., yn>,希望找出X和Y的LCS。我们可以用动态规划来有效解决。

步骤1:描述一个LCS

解决LCS问题的一种强力方法是枚举X所有子序列,逐一检查是否为Y的子序列,并记录所发现的最长子序列。对于有m个元素的序列X,一共有2m个子序列,显然不切实际。

然而LCS问题具有最优子结构性质。给定一个序列 X = <x1, x2, ..., xm>,对于i = 0, 1, ..., m,定义X的第i个前缀为 Xi = <x1, x2, ..., xi>。
例如,如果 X = <A, B, C, B, D, A, B>,则 X4 = <A, B, C, B>,而X0是一个空序列。

定理(LCS的最优子结构):设 X = <x1, x2, ..., xm> 和 Y = <y1, y2, ..., yn> 为两个序列,并设 Z = <z1, z2, ..., zk> 为X和Y的任意一个LCS。
1)如果 xm = yn, 那么 zk = xm = y且 Zk-1 是 Xm-1 和 Yn-1 的一个LCS;
2)如果 xm != yn,那么 zk != xm 蕴含 Z 是 Xm-1 和 Y 的一个LCS;
3)如果 xm != yn,那么 zk != yn 蕴含 Z 是 Yn-1 和 X 的一个LCS。

证明:这他妈说了个啥。
1)如果 zk != xm ,则可以添加 xm = y到Z中,即得到X和Y的一个长度为k+1的公共子序列,与Z是X和Y的LCS矛盾,因而必有 zk = xm = yn。并且此时前缀 Zk-1 是 Xm-1 和 Yn-1 的长度为k-1的公共子序列。假设 Xm-1 和 Yn-1 有一个长度大于k-1的公共子序列W,那么 xm = y添加到W上就会产生一个长度大于k的公共子序列,因而与Z是X和Y的LCS矛盾。得证。
2)如果 zk != xm 那么Z是 Xm-1 和 Y 的一个LCS。如果 Xm-1 和 Y 有一个长度大于k的公共子序列W,则W也应该是 Xm 和 Y 的一个公共子序列,这与Z为X和Y的LCS的假设矛盾。得证。
3)与证明2)对称,得证。

解释:这条定理的特征说明两个序列的一个LCS也包含了两个序列的前缀的一个LCS。这就说明LCS问题具有最优子结构性质。

步骤2:一个递归解

根据定理我们可知在找 X = <x1, x2, ..., xm> 和 Y = <y1, y2, ..., yn> 的一个LCS时,可能要检查一个或两个子问题。即:
如果 xm = y,必须找出 Xm-1 和 Yn-1 的一个LCS。将 xm = y添加到这个子LCS上,产生X和Y的一个LCS;
如果 xm != yn,就必须解决两个子问题:找出 Xm-1 和 Y 的一个LCS,以及 Yn-1 和 X 的一个LCS。这两个LCS中,较长的就是X和Y的一个LCS。

很容易看出LCS问题中重叠子问题性质。定义c[i,j]为序列Xi和Yi的一个LCS长度。如果i=0或j=0,其中一个的序列长度为0,因而LCS长度为0。由此得递归方程:

$\textrm{c}[i,j]= \begin{cases} 0&,\ i = 0\or\ j=0\\ c[i-1,j-1]+1&,\ i,j>0 \or\ x_{i}=y_{i}\\ \textrm{max}(c[i,j-1],c[i-1,j])&,\ i,j>0 \or\ x_{i}\neq y_{i} \end{cases}$

步骤3:计算LCS长度

过程LCS-LENGTH以两个序列 X = <x1, x2, ..., xm> 和 Y = <y1, y2, ..., yn> 为输入。运行时间为O(mn)。
它把c[i,j]的值填入一个按行计算表项的表c[0..m, 0..n]中(如下图);
它还维护表b[1..m, 1..n]以简化最优解的构造,b[i,j]指向一个表项,对应于在计算c[i,j]时所选择的最优子问题的解;
它返回表b和c;c[m,n]即为X和Y的一个LCS长度。

LCS-LENGTH(X, Y)
// 初始化表项
m = length[X]
n = length[Y]
for i = (0 to m) c[i,0] = 0
for j = (0 to n) c[0,j] = 0
// 循环计算LCS长度
for i = (1 to m)
    for j = (1 to n)
        if (x[i] = y[j])
            c[i,j] = c[i-1,j-1] + 1
            b[i,j] = &b[i-1,j-1]
        else if (c[i-1,j] >= c[i,j-1])
            c[i,j] = c[i-1,j]
            b[i,j] = &b[i-1,j]
        else
            c[i,j] = c[i,j-1]
            b[i,j] = &b[i,j-1]
// 返回结果
return b and c

步骤4:构造一个LCS

我们可以很容易地根据表b的结果快速构造X和Y的LCS。首先从b[m,n]开始,根据指针跟踪下去。每当遇到一个指向左上方的指针时,意味着 xm = y是LCS的一个元素,输出尔后继续追踪。该跟踪过程运行时间为O(m+n)。

改进代码

我们可以完全去掉表b,每个表项c[i,j]仅依赖于另外三个c的表项,即c[i-1,j-1], c[i-1,j], c[i,j-1]。给定c[i,j]的值,我们可以在O(1)时间内确定这三个值中哪一个是被用来计算c[i,j]的,而不用检查表b。这样我们就可以在同样O(m+n)时间内重构一个LCS。这种方法节省了O(mn)的空间。但并没有节省运行时间。当然,我们可以更激进一些,如果不需要重构LCS,只要求结果的话,我们只要c中两行数据就可以计算。因而进一步节省了空间。

时间: 2024-11-11 05:50:41

[CLRS][CH 15.4] 最长公共子序列的相关文章

P3402 最长公共子序列(nlogn)

P3402 最长公共子序列 题目背景 DJL为了避免成为一只咸鱼,来找Johann学习怎么求最长公共子序列. 题目描述 经过长时间的摸索和练习,DJL终于学会了怎么求LCS.Johann感觉DJL孺子可教,就给他布置了一个课后作业: 给定两个长度分别为n和m的序列,序列中的每个元素都是正整数.保证每个序列中的各个元素互不相同.求这两个序列的最长公共子序列的长度. DJL最讨厌重复劳动,所以不想做那些做过的题.于是他找你来帮他做作业. 输入输出格式 输入格式: 第一行两个整数n和m,表示两个数列的

序列最的问题之最长公共子序列LCS

在程序设计竞赛中,我们时常会遇到序列求最值的问题.在讲今天的问题之前,先小小的说明一下,子序列与子串的问题. 子序列:在原序列中不一定连续: 子串:在原序列中必须连续. 接下来,就开始今天要讲的最长公共子序列LCS(Longest Common Subsequence).对于LCS这一类的问题,一般是相对于两个序列而言,str[]与ch[].先假设str的长度为n,ch的长度为m.假设str[]="ASBDAH",ch[]="SDAAH";其中"SDA&q

洛谷 P1439 【模板】最长公共子序列

神TM模板..我本来想休闲一下写点水题的... 开始做的时候直接敲了一个O(N2)的算法上去,编译的时候才发现根本开不下.. 好了,谈回这道题. 先不加证明的给出一种算法. 若有一组数据 2 4 2 5 1 3 2 5 4 1 3 那么我们令 4 2 5 1 3 | | | | | 1 2 3 4 5 第三行的数据就变成 2 3 1 4 5 很明显,答案是这个数据的最长上升子序列,即4 == 2 3 4 5,即原数列的2 5 1 3. 现在来大概的介绍一下这样做的原因. 首先,观察题目,注意到这

【luogu1439】 【模板】最长公共子序列 [动态规划][LIS最长上升子序列][离散化]

P1439 [模板]最长公共子序列 此思路详见luogu第一个题解 一个很妙的离散化 刘汝佳蓝书上面的LIS 详见蓝书 d[i]以i为结尾的最长上升子序列的长度     g[i]表示d值为i的最小状态的编号即长度为i的上升子序列的最小末尾值 1 for(int i=1;i<=n;++i) scanf("%d",&a[i]); 2 for(int i=1;i<=n;++i) 3 { 4 int k=lower_bound(g+1,g+1+n,a[i])-g; 5 d[

dp--P1439 最长公共子序列(LCS)

题目描述 给出1-n的两个排列P1和P2,求它们的最长公共子序列. 输入格式 第一行是一个数n, 接下来两行,每行为n个数,为自然数1-n的一个排列. 输出格式 一个数,即最长公共子序列的长度 找出两个序列共同出现的元素,每个元素包括两个维度,一个为在a中的位置,一个为在b中的位置,我们首先保证一个序列在a中的位置单调递增,那么只要这个序列在b中得位置也单调递增,他们就是最长公共子序列. 代码如下: 1 #include <cstdio> 2 #include <iostream>

最长公共子序列的代码实现

关于最长公共子序列(LCS)的相关知识,http://blog.csdn.net/liufeng_king/article/details/8500084 这篇文章讲的比较好,在此暂时不再详说. 以下是我代码实现两种方式:递归+递推: 1 #include <bits/stdc++.h> 2 using namespace std; 3 int A[100]; 4 int B[100]; 5 6 //int B[]={2,3,5,6,9,8,4}; 7 int d[100][100]={0};

hdu 1159 Common Subsequence(dp 最长公共子序列问题LCS)

最长公共子序列问题(LCS,Longerst Common Subsequence). s1s2……si+1和t1t2……tj+1的公共子序列可能是: ①当si+1=tj+1时,在s1s2……si+1和t1t2……tj+1的公共子序列末尾追加一个. ②s1s2……si+1和t1t2……tj的公共子序列 ③s1s2……si和t1t2……tj+1的公共子序列 所以易得到递推关系dp[i+1][j+1]=  max{ dp[i][j]+1 , dp[i][j+1] , dp[i+1][j]) }  

经典dp 最长公共子序列

首先,说明一下子序列的定义…… 一个序列A={a1,a2,a3,...,an},从中删除任意若干项,剩余的序列叫A的一个子序列. 很明显(并不明显……),子序列……并不需要元素是连续的……(一开始的时候思维总是以为元素是连续的,好傻啊……) 然后是公共子序列…… 如果C是A的子序列,也是B的子序列,那么C是A和B的公共子序列…… 公共子序列一般不止一个,最长的那个就是最长公共子序列,当然也可能不止一个…… 煮个栗子…… A={1,3,6,9,5,4,8,7},B={1,6,3,4,5,7} {1

算法学习 - 最长公共子序列(LCS)C++实现

最长公共子序列 最长公共子序列的问题很简单,就是在两个字符串中找到最长的子序列,这里明确两个含义: 子串:表示连续的一串字符 . 子序列:表示不连续的一串字符. 所以这里要查找的是不连续的最长子序列, 动态规划 这里为什么要使用动态规划可以说一下,简单来说动态规划是为了降低时间复杂度的一种算法,申请一个额外空间,来保存每一个步骤的结果,最后从这些结果中找到最优的解. 这里有个问题就是:一般来说,当前的最优解,只与当前时刻和上一时刻有关系,和其他时刻没有关系,这样才能让动态规划发生作用,降低复杂度