LCS问题
1. 简介
LCS通常是指Longest Common Subsequence, 但是也可代指Longest Common Substring。子串是一种特殊的子序列,子串和子序列的区别就是字串要求是组成子串的
各字符是连续的,而子序列仅仅要求各字符的下标是递增的即可。举个例子,如helloworld
, world
是子串(也是子序列), hw
不是子串是子序列。
LCS问题就是在若干(这里是两个)字符串中,找到最长的一个字符串p, 这个p是那些字符串的子串(子序列)。LCS问题的意义之一就是去衡量若干字符串的相似度,例如在生物信息学
中有DNA序列的问题,比如序列a = "ATGATAGATAGATAG"
, 序列b="TGGGCCGAGAAGCGAGA"
,需要一种去衡量a
、b
相似度的方法,那么最长公共子序列就可以作为一种衡量方法。
除此之外,在软件开发领域中的版本控制系统(典型的就是Git
),比较同一个文件不同时刻的差异的时候,就需要利用到这个方法。
例如下图就是Git
上面的两次不同时刻对同一个文件的快照的对比图,我们可以看到,系统把两次的差异标示出来了。而原来位置、内容相同的部分没有标示。这个就可以通过
LCS
问题的解决,来找到最长的公共子序列部分,就是作为公共的、未发生改变部分;剩下的就是改变的部分,应该标示出来。
同理基于这个,我们还可以简单的比较两篇文章的相似度来检查雷同、抄袭的情况,这么看来,LCS问题的还是和我们日常比较相关的。
解决LCS问题依靠暴力(brute force)的求解时间复杂度是指数级别的,因此不太现实,由于问题的本身的最优解具有最优子结构特征,因此原问题的求解可以化为多个子问题的求解;另外子问题的求解存在重叠的情况,这恰好是适合动态规划求解的问题的第二个特征:存在重叠的子问题求解过程,因此适合利用动态规划来求解,这个也是较常见的解决LCS问题的方法,时间复杂度为O(n*m)
,动态规划易于编写,但是时间复杂度较高;另外更高级的还存在一种线性复杂度的O(n+m)
的解法,就是Suffix Tree
,不过这种方法的缺点是程序难以编制。因此在日常的编码中,动态规划已经够用了。
2. 最长公共子串问题
设dp[i][j]表示s[0~i]和t[0~j]的最长公共子串的长度,对于两个字符串的最长公共子串问题的最优解,最优解存在如下结构:
dp[i][j] = 1 (i == 0 || j == 0)
dp[i][j] = dp[i-1][j-1] + 1;(s[i] == t[j] && i > 0 && j > 0);
dp[i][j] = 0;(s[i] != t[j])
上述等式建立了原问题和子问题的关系(此类证明可用反证法),剩下的就是编码了。
public static List<String> findLongestLCS(String s, String t) {
char[] schs = s.toCharArray();
char[] tchs = t.toCharArray();
int m = s.length(), n = t.length();
int[][] dp = new int[m][n];
int maxSize = 0;
List<Integer> indexs = new ArrayList<>();//存储最长公共子串的在原始串中结束位置
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++) {
if (schs[i] == tchs[j]) {
if (i == 0 || j &