所谓LCS,即 “Longest Common Subsequence”,源于这样一个问题:求两个字符串最长公共子串,比如字符串 "edtestlcsbbca” 与字符串 "fbaatestlcsce” 的最长公共子串就是 "testlcs".
这个问题推而广之就是求两个序列中相同子序列中最长的若干个,常规的做法是,顺次以序列一中的一个元素(这里指字符)依次与序列二中的每一个元素相匹配,若相同则记录下从两序列中这个元素开始匹配的相同子序列... 完毕时,从这些子序列集合中找出最长的,那就是答案了。稍微优化后,就会发现没必要将匹配的全部子序列记录下来——将每次记录下的结果与前次记录的结果比较,若比前次更符合要求则替换之,否则丢弃这次匹配结果(若相同则记录下来——考虑可能有多个相同的最长公共子序列)。这是解决这个问题基本思路。
现在的问题时,怎样记录每次匹配的结果?表格应该是最好的方式。将两个序列中的字符分别分散在表格的第一列与最后一行,若某行与某列对应的元素相同则记为1 ,否则记为0。这样对角线上就是两个序列所有子序列相区配的结果(1和0组成的序列),而“1”序列最长的对角线对应的位置就是最长匹配子串的位置. 举个例子 字符串21232523311324和字符串312123223445的匹配的结果如下:(红色部分是最长的匹配子串其对应的子串为:21232)
3 0 0 0 1 0 0 0 1 1 0 0 1 0 0
1 0 1 0 0 0 0 0 0 0 1 1 0 0 0
2 1 0 1 0 1 0 1 0 0 0 0 0 1 0
1 0 1 0 0 0 0 0 0 0 1 1 0 0 0
2 1 0 1 0 1 0 1 0 0 0 0 0 1 0
3 0 0 0 1 0 0 0 1 1 0 0 1 0 0
2 1 0 1 0 1 0 1 0 0 0 0 0 1 0
2 1 0 1 0 1 0 1 0 0 0 0 0 1 0
3 0 0 0 1 0 0 0 1 1 0 0 1 0 0
4 0 0 0 0 0 0 0 0 0 0 0 0 0 1
4 0 0 0 0 0 0 0 0 0 0 0 0 0 1
5 0 0 0 0 0 1 0 0 0 0 0 0 0 0
x 2 1 2 3 2 5 2 3 3 1 1 3 2 4
现然记录方式也确定了,但似乎还有一个问题,对了,查找!在这个表格中查找最长的1序列对角线要花去一定的时间。思考一个问题:记录与查找两个步骤中都要对全表遍历一次,实在不划算,能否在一次遍历过程中同时做记录和查找的工作?其实我们上面初次提到的常规思路中红色部分就暗示了这个方法的可行性。落到实处就是在每次记录0或1时,顺便看看对角线上与之相邻的位置是否为1,若不是则不必理会,若是则将对角线相邻位置的记录加1的结果记录在这个位置——表示这个对角线上已有这么长的1序列,同时跟踪记录这个位置——若这个值比之前记录的这类值大,则替换之前记录的所有这类值,否则不理会(若相同则记录下来——考虑可能有多个相同的最长公共子串 )看这蓝色部分与之前的红色部分多相似:) ,最后勿须遍历全表就可通过这个记录找到最长的公共子序列了。
0 0 0 1 0 0 0 1 1 0 0 1 0 0
0 1 0 0 0 0 0 0 0 2 1 0 0 0
1 0 2 0 1 0 1 0 0 0 0 0 1 0
0 2 0 0 0 0 0 0 0 1 1 0 0 0
1 0 3 0 1 0 1 0 0 0 0 0 1 0
0 0 0 4 0 0 0 2 1 0 0 1 0 0
1 0 1 0 5 0 1 0 0 0 0 0 2 0
1 0 1 0 1 0 1 0 0 0 0 0 1 0
0 0 0 2 0 0 0 2 1 0 0 1 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 1 0 0 0 0 0 0 0 0
再仔细瞧瞧,我们是依次按行从左向右记录的,每次跟踪所需的数据仅是前一行,这样大量记录都没有用了,留着是浪费空间。 优化一下 , 其中一行记录匹配状态,另外需要一行记录跟踪状态,一行记录跟踪状态对应的字串位置。
用中语实现这个算法如下:
公有 接口 处理方式{
空白 算符(字符[] 序列,示例算法 结果);
}
公有 元 示例算法{
中整[] 跟踪状态;
中整[] 跟踪字串位置;
公有 空白 查寻(字串 字串A, 字串 字串B, 处理方式 结果处理方法) //空白[字符[],示例算法]
{
字符[] 字串1 = 字串A.toCharArray();
字符[] 字串2 = 字串B.toCharArray();
中整 匹配状态个数 = 字串1.length > 字串2.length?字串1.length:字串2.length;
中整[] 匹配状态 = 新 中整[匹配状态个数];
跟踪状态= 新 中整[匹配状态个数];
跟踪字串位置= 新 中整[匹配状态个数];
循环 (中整 i=0; i < 字串2.length ; i++)
{
循环 (中整 j=字串1.length -1; j >= 0; j--)
{
若 (字串2[i] == 字串1[j])
{
若 ( ( i == 0) || (j == 0) )
匹配状态[j] = 1;
否则
匹配状态[j] = 匹配状态[j-1] + 1;
} 否则 匹配状态[j] = 0;
若 (匹配状态[j] > 跟踪状态[0])
{ //表示找到一个更长的
跟踪状态[0] = 匹配状态[j];
跟踪字串位置[0] = j;
//,因而要将原先的记录全部清0
循环 (中整 k = 1; k < 匹配状态个数; k++)
{
跟踪状态[k] = 0;
跟踪字串位置[k] = 0;
}
} 否则 若 (匹配状态[j] == 跟踪状态[0]) //表示出现了多个相同长度的公共子串,将这个记录到跟踪状态中
循环 (中整 k = 1; k < 匹配状态个数; k++)
若 (跟踪状态[k] == 0){
跟踪状态[k] = 匹配状态[j];
跟踪字串位置[k] = j;
中止;
}
//若 匹配状态[j] < 跟踪状态[0] 则不必理会继续下轮的匹配
}
}
//------------------------------------------
结果处理方法(字串1,this);
}
}