算法课要交个算法的作业,打算找个动态规划的顺便复习一下,就找了一个经典例题——最长公共子序列(LCS),正好检验一下自己的python水平有没有进步...
问题描述:
给定两个字符串A和B,返回两个字符串的最长公共子序列,并记录最长公共子序列的长度。例如,两个字符串A、B分别为“1A2C3D4B56”和“B1D23CA45B6A”,则“123456”和“12C4B6”都是最长公共子序列,且最长公共子序列长度为6。(最长公共子序列可能不唯一,但长度唯一。)
解题思路:动态规划
设两个字符串A、B长度分别为m、n,用A[i]、B[j]表示两个字符串第i和第j个字符。
自顶向下分析:根据两个序列最后一个字符是否匹配分两种情况:若匹配,加入这一字符必然可以达到最大长度,长度为A[m-1]、B[n-1]的最长公共子序列的长度+1;若不匹配,则最长公共子序列应是A[m-1]、B[n]或A[m]、B[n-1]的最长公共子序列。即:
分别对应两个序列最后一个字符是否匹配的两种情况。这就是动态规划的状态转移关系。中间的字符也根据这个关系类推。值得注意的是,可能存在不同的最长公共子序列,但是最大值只有一个。
自底向上操作:从i、j = 0 开始,根据状态转移方程,逐步推导至i = m、j = n的各个状态的最优解:即A、B的某两段的最长公共子序列记录成一个矩阵。如下例,两个序列分别为“13456778”和“357486782”。
编程实现:采用python语言。
首先根据两个字符串的长度生成这样一个矩阵。这个矩阵的每个数表示当前状态最长公共子序列的长度。显然,矩阵右下角的元素就给出了A和B的最大公共子序列长度。函数在程序中实现如下:
下面我们将根据已有的矩阵寻找到最长公共子序列。显然,我们只需要关注那些匹配的字符,因为公共子序列必然是由在两个字符串都出现的字符组成的。之所以寻找子序列和计算长度分开进行,是因为存在着不同的“路径”可以到达最终右下角的最大长度。如该例存在3个子序列:“34678”、“35678”、“35778”。
根据观察,可以定义“有效”匹配字符。也即,某一字符匹配后,会使得最长公共子序列的长度加1,这个额外的限制条件保证程序在搜索匹配字符时不会被重复的字符干扰。将图中有效匹配字符圈出来,即可找到多条路径,但这些匹配字符形成的子序列要满足顺序的要求:更大长度的点要出现较小长度点的右下方。因此,本来一共有很多组合方式,但因为其中大部分不满足这个原则被删掉,最终得到三条可能路径,也即3个最大长度子序列。
接下来,我们编程实现寻找最大长度子序列。首先找到所有的有效匹配字符,即圈出来的那些,保存他们的值、位置、和代表的字符。该函数的程序如下:
为了更好地遍历搜索,调整列表顺序,使其按照值从小到大排序,并生成一个二维列表。由于列表长度远小于矩阵规模,生成列表后再进行搜索可以提高算法效率。程序如下:
随后就是生成所有最长公共子序列的程序:我们每次“成功”添加一个新的字符后都会更新最长子序列列表,就像生成矩阵时所做的那样,从长度为1的子序列开始,逐步生成最长的公共子序列。
题目描述中“1A2C3D4B56”和“B1D23CA45B6A”的测试结果:
即这两个字符串存在4个最长公共子序列,分别为“12C4B6”、“12C456”、“1234B6”、“123456”。