实验三:最长公共子序列
一、问题描述:
子序列形式化定义:
给定一个序列X=<x1,x2,x3,x4...,xm>,另一个序列Z=<z1,z2,z3,z4...,zk>,若存在一个严格递增的X的下标序列<i1,i2,i3,...,ik>对所有的1,2,3,...,k,都满足x(ik)=zk,则称Z是X的子序列。
比如Z=<B,C,D,B>是X=<A,B,C,B,D,A,B>的子序列。
公共子序列定义:
如果Z既是X的子序列,又是Y的子序列,则称Z为X和Y的公共子序列。
最长公共子序列(以下简称LCS):
2个序列的子序列中长度最长的那个。
我们需要编写代码,找到给定两个序列的最长公共子序列。
二、实验环境:
系统:Windows 10
IDE:Visual Studio 2022
编译语言:C++
三、程序代码与结果分析:
(1)分析问题:
蛮力法求解最长公共子序列:
需要遍历出所有的可能,时间复杂度是O(n³)。
动态规划求解最长公共子序列:
分析规律:
设X=<x1,x2,x3,x4...,xm>,Y=<y1,y2,y3,y4...,yn>为两个序列,Z=<z1,z2,z3,z4...,zk>是他们的任公共子序列,经过分析,我们可以知道:
1、如果xm = yn,则zk = xm = yn 且 Zk-1是Xm-1和Yn-1的一个LCS
2、如果xm != yn 且 zk != xm,则Z是Xm-1和Y的一个LCS
3、如果xm != yn 且 zk != yn,则Z是X和Yn-1的一个LCS
所以如果用一个二维数组c表示字符串X和Y中对应的前i,前j个字符的LCS的长度话,可以得到以下公式:
(2)递推关系的建立
因此,我们只需要从c[0][0]开始填表,填到c[m-1][n-1],所得到的c[m-1][n-1]就是LCS的长度。
但是,我们怎么得到LCS本身而非LCS的长度呢?也是用一个二维数组b来表示:
在对应字符相等的时候,用1标记;在p1 >= p2的时候,用2标记;在p1 < p2的时候,用3标记。
- 动态方程建立
下面我们以两组序列X=“AACBCDB”,Y=“ACBDAB”为例,描述一下填表的过程。
显然,矩阵c和b是一个7*6的矩阵,我们先填矩阵c。
刚开始时,初始化c表的值为全0,防止出现指向不明的问题
0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 |
我们现在开始比较X和Y。我们先拿出X序列的第一个A与Y序列进行一一对比,如果两者相等,则该位置的值将更改为他的对角线上方的值加一,如例子中的两组序列,他们的第一个字符相同,则c[1][1]的值将更改为c[0][0]+1=1;如果不相同,那么该位置的值将在这个格子的左方或者上方中选择一个更大的数填在表格中,直到X序列全部和Y序列比较完毕执行结束。
下表为填充结束后的矩阵。
A | C | B | D | A | B | ||
0 | 0 | 0 | 0 | 0 | 0 | 0 | |
A | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
A | 0 | 1 | 1 | 1 | 1 | 2 | 2 |
C | 0 | 1 | 2 | 2 | 2 | 2 | 2 |
B | 0 | 1 | 2 | 3 | 3 | 3 | 3 |
C | 0 | 1 | 2 | 3 | 3 | 3 | 3 |
D | 0 | 1 | 2 | 3 | 4 | 4 | 4 |
B | 0 | 1 | 2 | 3 | 4 | 4 | 5 |
而表格的最后一位数字,就是LSC的长度。
若想得到LCS,则再遍历一次b数组就好了,从最后一个位置开始往前遍历:如果箭头是1,则代表这个字符是LCS的一员,存下来后 i-- , j--;如果箭头是2,则代表这个字符不是LCS的一员,j--;如果箭头是3 ,也代表这个字符不是LCS的一员,i--;如此直到i = 0或者j = 0时停止,最后存下来的字符就是所有的LCS字符。
下表为矩阵b的内容,
0 | 0 | 0 | 0 | 0 | 0 |
0 | 1 | 3 | 3 | 3 | 3 |
0 | 1 | 2 | 2 | 2 | 3 |
0 | 2 | 1 | 3 | 3 | 2 |
0 | 2 | 2 | 1 | 3 | 1 |
0 | 2 | 1 | 2 | 2 | 2 |
0 | 2 | 2 | 2 | 1 | 3 |
0 | 2 | 2 | 1 | 2 | 1 |
所以很容易,我们就可以得到示例中的LSC为:ACBDB
由于只需要填一个m行n列的二维数组,其中m代表第一个字符串长度,n代表第二个字符串长度
所以时间复杂度为O(m*n)
(4)代码分析
首先,我们定义了一个资源文件“test.txt”,用于储存实验所需的测试数据。
内容如下所示:
我们可以看到,第一行输入测试数据的条数,之后都是每三行中的第一行为序列长度,第二行第三行则为序列。
我们同时定义了一系列模板,在“create_and_delete.h”中,如下图所示:
包含了创建矩阵和向量,打印矩阵和向量以及释放矩阵和向量的空间。
同时,我们在这个头文件中,也定义了两个关键的函数,LSClength和Print_LSC,前者的作用是计算出矩阵c和矩阵b的值,后者的作用是遍历矩阵b,打印出最长公共子序列。
在源cpp文件中:
首先是文件流的创建:
之后是各项参数的定义以及初始化:
接着就是调用关键的函数,计算最长公共子序列以及输出:
最后就是把一开始申请的数组空间给释放掉:
(5)代码运行结果实例
从图中,我们可以看出,计算结果与我们手算的结果是完全一致的,重复测试多组不同数据也可以得到正确结果。
四、实验中遇到的问题以及实验体会:
这次的实验是做最长公共子序列的实验,在实验过程中,遇到了很多问题。
第一就是在用文件输入的时候,每次系统都会报错,结果经过检查后发现,每次打开文件之后必须关闭文件,我在调试的时候检查打开了文件忘记关闭文件,所以导致了错误。
第二就是在构建矩阵空间的时候,最开始我构建的是一个m*n大小的矩阵空间,但是每次运行到一半,程序就会报错,说指针指向了未知空间,经过多次检查,最终发现是矩阵空间申请少了,我们手过一遍程序过程,我们可以发现,m长度的X序列和n长度的Y序列,要遍历他们的矩阵空间必须是(m+1)*(n+1),才可以正常运行。
第三就是在遇到没有公共子序列的两组序列,我们想要输出无公共子序列,最开始我是直接在PrintLSC中加了一行判断,如果c[i][j]=0的话,输出。但是运行的时候发现,每次都会输出无公共子序列,我很疑惑,最后突然发现,我们每次递归调用的最后,i和j总是等于0,然后退出递归调用,但是我们的c矩阵的第一行和第一列全为0,所以不管我们怎么调用,到最后一定会有c[i][j]=0的情况,这个时候,我们只需要把判断语句放到结束递归的后面,就可以成功运行了。
最后就是在写代码的过程中,模板的定义还有申请空间记不太清了,在网上学习了一些c++的知识后,逐渐回忆起来了,同时也学习了二维数组以及二级指针作为形参是,实参可以有哪几种调用方法。
当二级指针作为函数形参时,能作为函数实参的是二级指针,指针数组,一级指针的地址;当数组指针作为函数形参时,能作为函数实参的是二维数组,数组指针;当二维数组作为函数形参时,能作为函数实参的是二维数组,数组指针;当指针数组作为函数形参时,能作为函数实参的是指针数组,二级指针,一级指针的地址
完整实验可运行可提交代码和格式化报告请见如下链接: