技术心得记录:最长公共子序列(LCS)详解+例题模板(全)(转)

简介: 技术心得记录:最长公共子序列(LCS)详解+例题模板(全)(转)

1.摘要:


继上篇最长上升子序列后,本篇主要讲述最长公共子序列 (LCS) 。


2.LCS定义:


最长公共子序列,英文缩写为LCS(Longest Common Subsequence)。其定义是,一个序列 S ,如果分别是两个或多个已知序列的子序列,且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列。


如果觉得抽象不好理解,那么咱们还是采用学习LIS的时候的方式。首先,让我们先来看一下子串、子序列还有公共子序列的概念(在上篇LIS中也曾涉及过) ,我们以字符子串和字符子序列为例,更为形象,也能顺带着理解字符的子串和子序列:


(1)字符子串:指的是字符串中连续的n个字符,如abcdefg中,ab,cde,fg等都属于它的字串。


(2)字符子序列:指的是字符串中不一定连续但先后顺序一致的n个字符,即可以去掉字符串中的部分字符,但不可改变其前后顺序。如abcdefg中,acdg,bdf属于它的子序列,而bac,dbfg则不是,因为它们与字符串的字符顺序不一致。


(3) 公共子序列:如果序列C既是序列A的子序列,同时也是序列B的子序列,则称它为序列A和序列B的公共子序列。如对序列 1,3,5,4,2,6,8,7和序列 1,4,8,6,7,5 来说,序列1,8,7是它们的一个公共子序列。


那么现在,我们再通俗的总结一下最长公共子序列(LCS):就是A和B的公共子序列中长度最长的(包含元素最多的)


仍然用序列1,3,5,4,2,6,8,7和序列1,4,8,6,7,5为例,它们的最长公共子序列有1,4,8,7和1,4,6,7两种,但最长公共子序列的长度是4。由此可见,最长公共子序列(LCS)也不一定唯一。


请大家用集合的观点来理解这些概念,子序列、公共子序列以及最长公共子序列都不唯一,所以我们通常特判取一个最长公共子序列,但很显然,对于固定的两个数组,虽然最LCS不一定唯一,但LCS的长度是一定的。查找最长公共子序列与查找最长公共子串的问题不同的地方在于:子序列不需要在原序列中占用连续的位置。最长公共子串(要求连续)和最长公共子序列是不同的。


那么该如何求出两个序列的最长公共子序列长度呢?请继续往下看~


3.LCS长度求法:


你首先能想到的恐怕是暴力枚举?那我们先来看看:序列A有 2^n 个子序列,序列B有 2^m 个子序列,如果任意两个子序列一一比较,比较的子序列高达 2^(n+m) 对,这还没有算具体比较的复杂度。或许你说,只有长度相同的子序列才会真正进行比较。那么忽略空序列,我们来看看:对于A长度为1的子序列有C(n,1)个,长度为2的子序列有C(n,2)个,……长度为n的子序列有C(n,n)个。对于B也可以做类似分析,即使只对序列A和序列B长度相同的子序列做比较,那么总的比较次数高达:C(n,1)C(m,1)1 + C(n,2) C(m,2) 2+ …+C(n,p) C(m,p)p,其中p = min(m, n)。


吓着了吧?怎么办?我们试试使用动态规划算法!


我们用Ax表示序列A的连续前x项构成的子序列,即Ax= a1,a2,……ax, By= b1,b2,……by, 我们用LCS(x, y)表示它们的最长公共子序列长度,那原问题等价于求LCS(m,n)。为了方便我们用L(x, y)表示Ax和By的一个最长公共子序列。让我们来看看如何求LCS(x, y)。我们令x表示子序列考虑最后一项


(1) Ax = By


那么它们L(Ax, By)的最后一项一定是这个元素!


为什么呢?为了方便,我们令t = Ax = By, 我们用反证法:假设L(x,y)最后一项不是t,则要么L(x,y)为空序列(别忘了这个),要么L(x,y)的最后一项是Aa=Bb ≠ t, 且显然有a < x, b < y。无论是哪种情况我们都可以把t接到这个L(x,y)后面,从而得到一个更长的公共子序列。矛盾!


如果我们从序列Ax中删掉最后一项ax得到Ax-1,从序列By中也删掉最后一项by得到By-1,(多说一句角标为0时,认为子序列是空序列),则我们从L(x,y)也删掉最后一项t得到的序列是L(x – 1, y - 1)。为什么呢?和上面的道理相同,如果得到的序列不是L(x - 1, y - 1),则它一定比L(x - 1, y - 1)短(注意L(,)是个集合!),那么它后面接上元素t得到的子序列L(x,y)也比L(x - 1, y - 1)接上元素t得到的子序列短,这与L(x, y)是最长公共子序列矛盾。因此L(x, y) = L(x - 1, y - 1) 最后接上元素t,LCS(Ax, By) = LCS(x - 1, y - 1) + 1。


(2) Ax ≠ By


仍然设t = L(Ax, By), 或者L(Ax, By)是空序列(这时t是未定义值不等于任何值)。则t ≠ Ax和t ≠ By至少有一个成立,因为t不能同时等于两个不同的值嘛!


(2.1)如果t ≠ Ax,则有L(x, y)= L(x - 1, y),因为根本没Ax的事嘛。


LCS(x,y) = LCS(x – 1, y)


(2.2)如果t ≠ By,l类似L(x, y)= L(x , y - 1)


LCS(x,y) = LCS(x, y – 1)


可是,我们事先并不知道t,由定义,我们取最大的一个,因此这种情况下,有LCS(x,y) = max(LCS(x – 1, y) , LCS(x, y – 1))。看看目前我们已经得到了什么结论:


LCS(x,y) =


(1) LCS(x - 1,y - 1) + 1 (Ax = By)


(2) max(LCS(x – 1, y) , LCS(x, y – 1)) (Ax ≠ By)


这时一个显然的递推式,光有递推可不行,初值是什么呢?显然,一个空序列和任何序列的最长公共子序列都是空序列!所以我们有:


LCS(x,y) =


(1) LCS(x - 1,y - 1) + 1 如果Ax = By


(2) max(LCS(x – 1, y) , LCS(x, y – 1)) 如果Ax ≠ By


(3) 0 如果x = 0或者y = 0


到此我们求出了计算最长公共子序列长度的递推公式。我们实际上计算了一个(n + 1)行(m + 1)列的表格(行是0..n,列是0..m),也就这个二维度数组LCS(,)。


大概的伪代码如下:


输入序列A, B长度分别为n,m,计算二维表 LCS(int,int):


for x = 0 to n do


for y = 0 to m do


if (x == 0 || y == 0) then


LCS(x, y) = 0


else if (Ax == By) then


LCS(x, y) = LCS(x - 1,y - 1) + 1


else


LCS(x, y) = ) max(LCS(x – 1, y) , LCS(x, y – 1))


endif


endfor


endfor


注意: 我们这里使用了循环计算表格里的元素值,而不是递归,如果使用递归需要已经记录计算过的元素,防止子问题被重复计算。


现在问题来了,我们如何得到一个最长公共子序列而仅仅不是简单的长度呢?其实我们离真正的答案只有一步之遥!


仍然考虑那个递推式,我们LCS(x,y)的值来源的三种情况:


(1) LCS(x – 1, y – 1) + 1如果Ax = By


这对应L(x,y) = L(x,- 1 y- 1)末尾接上Ax


(2.1) LCS(x – 1, y) 如果Ax ≠ By且LCS(x – 1, y) ≥LCS(x, y – 1)


这对应L(x,y)= L(x – 1, y)


(2.2) LCS(x, y – 1) 如果Ax ≠ By且LCS(x – 1, y)

这对应L(x,y) = L(x, y – 1)


(3) 0 如果 x =0或者y = 0


这对应L(x,y)=空序列


注意(2.1)和(2.2) ,当LCS(x – 1, y) = LCS(x, y – 1)时,其实走哪个分支都一样,虽然长度时一样的,但是可能对应不同的子序列,所以最长公共子序列并不唯一。


神奇吧?又一个类似的递推公式。可见我们在计算长度LCS(x,y)的时候只要多记录一些信息,就可以利用这些信息恢复出一个最长公共子序列来。就好比我们在迷宫里走路,走到每个位置的时候记录下我们时从哪个方向来的,就可以从终点回到起点一样。


另外,说一下复杂度?//代码效果参考:http://hnjlyzjd.com/hw/wz_24950.html

时间复杂度时O(n m),空间也是O(n m)。

4.LCS经典例题模板:


例1:Common Subsequence(求LCS长度)


Description


A subsequence of a given sequence is the given sequence with some elements (possible none) left out. Given a sequence X = another sequence Z = is a subsequence of X if there exists a strictly increasing sequence of indices of X such that for all j = 1,2,...,k, xij = zj. For example, Z = is a subsequence of X = with index sequence . Given two sequences X and Y the problem is to find the length of the maximum-length common subsequence of X and Y.


The program input is from a text file. Each data set in the file contains two strings representing the given sequences. The sequences are separated by any number of white spaces. The input data are correct. For each set of data the program prints on the standard output the length of the maximum-length common subsequence from the beginning of a separate line.


Sample Input


abcfbc abfcab


programming contest


abcd mnp


Sample Output


4


2


0


思路:


题意是,称序列 Z = 是序列X = 的子序列当且仅当存在严格上升的序列,使得对j = 1, 2, ... ,k, 有xij = zj。比如Z = 是X = 的子序列。现在给出两个序列X 和Y,你的任务是找到X 和Y 的最大公共子序列,也就是说要找到一个最长的序列Z,使得Z 既是X 的子序列也是Y 的子序列。


其实就是模板题啦~求LCS长度,当A【i】=A【j】时d(I,j)d(i-1,j-1)+1,否则d(i,j)=max{ d(i-1,j),d(i,j-1) },时间复杂度为O(n*m),其中n和m分别是序列A和B的长度。


代码:


#include


#include


#include


#include


#include


#include


using namespace std;


char a【1001】, b【1001】;


int dp【1001】【1001】, len1, len2;


void lcs(int i,int j)


{


for(i=1; i<=len1; i++)


{


for(j=1; j<=len2; j++)


{


if(a【i-1】 == b【j-1】)


dp【i】【j】 = dp【i-1】【j-1】 + 1;


else if(dp【i-1】【j】 > dp【i】【j-1】)


dp【i】【j】 = dp【i-1】【j】;


else


dp【i】【j】 = dp【i】【j-1】;


}


}


}


int main()


{


while(~scanf(" %s",a))


{


scanf(" %s", b);


memset(dp, 0, sizeof(dp));


len1 = strlen(a);


len2 = strlen(b);


lcs(len1, len2);


printf("%d\n", dp【len1】【len2】);


}


return 0;


}


例2:最长公共子序列Lcs(求LCS具体是什么)


Description


给出两个字符串A B,求A与B的最长公共子序列(子序列不要求是连续的)。


比如两个串为:abcicba 和 abdkscab,则 ab是两个串的子序列,abc也是,abca也是,其中abca是这两个字符串最长的子序列。


Input


第1行:字符串A


第2行:字符串B


(A,B的长度 <= 1000)


Output


输出最长的子序列,如果有多个,随意输出1个。


Sample Input


abcicba


abdkscab


Sample Output


abca


思路:


此题的切入点就是动态规划,通过动归来确定哪些字符是最长公共子序列中的字符,mat【i】【j】 表示第一个序列的前i个字符和第二个序列的前j个字符的公共子序列,动态转移方程为:


dp【i】【j】 = max(dp【i-1】【j】, dp【i】【j-1】,dp【i-1】【j-1】 + (A【i】==B【j】 ? 1 : 0)),表示在这三种状态中取到最大值,


(1)第一种状态表示不录入第一个序列的第i个字符时的最长公共子序列,


(2)第二种状态表示不录入第二个序列的第j个字符时的最长公共子序列,


(3)第三种状态表示第一个序列的前i-1个字符与第二个序列前j-1个字符的公共子序列加上最后一个字符的录入状态,如果最后的一个字符相等则录入状态为1,否则为0。


然后根据动归的状态,来判断我们要求得的序列中的字符有哪些。


代码:


#include


#include


#include


#include


#include


#include


#include


#define INF 0x3f3f3f3f


using namespace std;


char a【1001】, b【1001】;


int dp【1001】【1001】, len1, len2;


void lcs(int i, int j)


{


for(i=1; i<=len1; i++)


{


for(j=1; j<=len2; j++)


{


if(a【i-1】 == b【j-1】)


dp【i】【j】 = dp【i-1】【j-1】 + 1;


else if(dp【i-1】【j】 > dp【i】【j-1】)


dp【i】【j】 = dp【i-1】【j】;


else


dp【i】【j】 = dp【i】【j-1】;


}


}


}


void llcs()


{


int i, j, z = 0;


char c【1001】;


memset(c, 0, sizeof(c));


i = len1, j = len2;


while(i!=0 && j!=0)


{


if(a【i-1】 == b【j-1】)


{


c【z++】 = a【--i】;


j--;


}


else if(dp【i-1】【j】 < dp【i】【j-1】)


j--;


else if(dp【i】【j-1】 <= dp【i-1】【j】)


i--;


}


for(i=z-1; i>=0; i--)


printf("%c", c【i】);


printf("\n");


}


int main()


{


while(~scanf(" %s", a))


{


scanf(" %s", b);


memset(dp, 0, sizeof(dp));


len1 = strlen(a);


len2 = strlen(b);


lcs(len1, len2);


llcs();


}


return 0;


}


例3:Advanced Fruits(根据LCS将两个词拼接)


Decription


The company "21st Century Fruits" has specialized in creating new sorts of fruits by transferring genes from one fruit into the genome of another one. Most times this method doesn't work, but sometimes, in very rare cases, a new fruit emerges that tastes like a mixture between both of them.


A big topic of discussion inside the company is "How should the new creations be called?" A mixture between an apple and a pear could be called an apple-pear, of course, but this doesn't sound very interesting. The boss finally decides to use the shortest string that contains both names of the original fruits as sub-strings as the new name. For instance, "applear" contains "apple" and "pear" (APPLEar and apPlEAR), and there is no shorter string that has the same property.


A combination of a cranberry and a boysenberry would therefore be called a "boysecranberry" or a "craboysenberry", for example.


Your job is to write a program that computes such a shortest name for a combination of two given fruits. Your algorithm should be efficient, otherwise it is unlikely that it will execute in the alloted time for long fruit names.


Input


Each line of the input contains two strings that represent the names of the fruits that should be combined. All names have a maximum length of 100 and only consist of alphabetic characters.


Input is terminated by end of file.


Output


For each test case, output the shortest name of the resulting fruit on one line. If more than one shortest name is possible, any one is acceptable.


Sample Input


apple peach


ananas banana


pear peach


Sample Output


appleach


bananas


pearch


思路:


在LCS的基础之上加上路径记录,生成dp数组的时候做上标记,之后按顺序输出结果字符串。注意还要考虑一下没有公共子序列的情况。


代码:


#include


#include


#include


#include


#include


#include


#include


using namespace std;


char a【1001】, b【1001】, s【10000】;


int dp【1001】【1001】, len1, len2, k = 0;


void lcs(int i,int j)


{


for(i=1; i<=len1; i++)


{


for(j=1; j<=len2; j++)


{


if(a【i-1】 == b【j-1】)


dp【i】【j】 = dp【i-1】【j-1】 + 1;


else if(dp【i-1】【j】 > dp【i】【j-1】)


dp【i】【j】 = dp【i-1】【j】;


else


dp【i】【j】 = dp【i】【j-1】;


}


}


}


void llcs()


{


int i, j, z = 0, k = 0;


i = len1, j = len2;


while(dp【i】【j】)


{


if(a【i-1】 == b【j-1】)


{


s【k++】=a【--i】;


j--;


}


else if(dp【i】【j-1】 < dp【i-1】【j】)


s【k++】 = a【--i】;


else if(dp【i】【j-1】 >= dp【i-1】【j】)


s【k++】 = b【--j】;


}


while(i != 0)


s【k++】 = a【--i】;


while(j!=0)


s【k++】 = b【--j】;


for(z=k-1; z>=0; z--)


printf("%c", s【z】);


printf("\n");


}


int main()


{


while(~scanf(" %s",a))


{


scanf(" %s", b);


memset(dp, 0, sizeof(dp));


len1 = strlen(a);


len2 = strlen(b);


lcs(len1, len2);


llcs();


}


return 0;


}


5.相关知识:( 建议放在一起比较区分 )


1)最长上升子序列 ( LIS ) 戳这里~


2)最长回文子串 and 最长回文子序列 (LPS) 戳这里 ~


转自:


联系方式:emhhbmdfbGlhbmcxOTkxQDEyNi5jb20=

相关文章
|
Java
SpringBoot导入导出Excel
简单、便捷、快速
8247 0
|
2月前
|
人工智能 JavaScript PHP
Laravel + Vue 免费可商用 PHP 管理后台 CatchAdmin V5.3.0 发布:支持 AI Agent 开发
CatchAdmin V5.3.0 是基于 Laravel 13 + Vue3 的免费可商用后台框架,支持企业级权限、动态路由、代码生成等;本次重磅升级AI开发体验,内置AGENTS指引、9大AI Skills、多平台兼容及Context7实时文档,大幅提升后台开发效率。(239字)
277 1
Laravel + Vue 免费可商用 PHP 管理后台 CatchAdmin V5.3.0 发布:支持 AI Agent 开发
|
19天前
|
人工智能 弹性计算 缓存
阿里云新老用户购买云服务器最新活动参考:轻量应用服务器秒杀、ECS长效特惠,热门实例折扣与价格
2026年阿里云新老用户购买云服务器的最新优惠活动:主要包括三大类:一是轻量应用服务器限时秒杀,2核2G低至38元/年、2核4G仅9.9元/月,每日10点和15点开抢,预置Hermes/OpenClaw镜像可分钟级部署AI应用;二是低价长效特惠,经济型e实例99元/年、通用算力u1实例199元/年,均支持续费同价,第九代实例月付8折、年付6.4折;三是新用户专享u2i实例3折优惠及学生无门槛券、企业AI算力券等叠加福利。
|
机器学习/深度学习 人工智能 算法
【算法】最长公共子序列(C/C++)
【算法】最长公共子序列(C/C++)
|
监控 安全 测试技术
|
算法
动态规划算法学习二:最长公共子序列
这篇文章介绍了如何使用动态规划算法解决最长公共子序列(LCS)问题,包括问题描述、最优子结构性质、状态表示、状态递归方程、计算最优值的方法,以及具体的代码实现。
844 0
动态规划算法学习二:最长公共子序列
|
人工智能 IDE 小程序
不写一行代码,通义灵码 5 分钟“手撕”年会抽奖程序
年会中的抽奖环节不可或缺,但每年为了选择合适的抽奖小程序,团队往往需要投入大量时间和精力。然而,抽奖结束后,参与者通常只记得自己是否中奖,其他细节多被遗忘。在 AI 技术日益成熟的今天,如何打造一个既高效又有技术含量的抽奖应用呢?今天,就让我们跟随通义灵码,仅用 5 分钟现场手撕一个抽奖应用吧!
|
Ubuntu Python
全网最简约的Vscode配置Anaconda环境(百分百成功)
全网最简约的Vscode配置Anaconda环境(百分百成功)
38411 0
全网最简约的Vscode配置Anaconda环境(百分百成功)
|
机器学习/深度学习 算法 数据挖掘
机器学习(十九)EM:期望最大算法
机器学习(十九)EM:期望最大算法
945 0
机器学习(十九)EM:期望最大算法
|
存储
【头歌·计组·自己动手画CPU】五、单总线CPU设计(理论版) 【计算机硬件系统设计】
【头歌·计组·自己动手画CPU】五、单总线CPU设计(理论版) 【计算机硬件系统设计】
3611 2