问题
给定两个序列
X = { x1 , x2 , ... , xm } Y = { y1 , y2 , ... , yn }求 X和 Y的一个最长公共子序列(LCS, Longest Common Sequence)
分析
一般典型的解法是利用动态规划,利用如下性质:
- 若xm = yn ,则 zk = xm = yn,且 Z[k-1] 是 X[m-1] 和 Y[n-1] 的最长公共子序列
- 若xm != yn ,且 zk != xm , 则 Z 是 X[m-1]和 Y 的最长公共子序列
- 若xm != yn , 且 zk != yn , 则 Z 是 Y[n-1] 和 X 的最长公共子序列
在一个二维表格上递推:
当 i = 0 , j = 0 时 , c[i][j] = 0
当 i , j > 0 ; xi = yi 时 , c[i][j] = c[i-1][j-1] + 1
当 i , j > 0 ; xi != yi 时 , c[i][j] = max { c[i][j-1] , c[i-1][j] }
算法复杂度为 O(mn),辅助空间也需要 O(mn)。后来也又不少改进算法,基本思想是通过发现一些规律,减少在表格上的递推次数。
解法一
这个问题其实也可以利用“分级排列法”求解,算法复杂度接近O(mlogn),辅助空间 O(m+n)。基本思路和本文“最长递增子序列”的解法类似:
在给定的 X序列中,求 1~m级的情况,每一级中保存与 Y的所有 LCS中,末尾元素(在 Y中位置)最低的那个。
假设 X=4,2,3,2,5,1,7,8,4,Y=1,2,3,4,8,2, 4,5,6,8,每级计算过程如下表:
那么计算过程可以解释为:
- 先从X中取出第一个元素 4,通过 yDic发现 Y中包含 4,位置为 3,6,那么长度为 1的公共子序列,末尾元素在 Y中最低位置为 3;
- 从X中取出第二个元素 2,通过 yDic发现 Y中包含 2,位置为 1,5,那么长度为 1的公共子序列,末尾元素在 Y中最低位置更新为 1。长度为 2的公共子序列,末尾元素在 Y中最低位置为 5;
- 以此类推,遇到Y中没有的元素,则直接略过。
关键代码如下:
int maxL = 0;
for (int i = 0; i < X.Count; i++)//处理 X中每一个元素
{
var x = X[i];
if (yDic.ContainsKey(x))//如果 Y中有相同元素 x
{
var pos = yDic[x];//获取 x在 Y中所有位置信息
for (int j = pos.Count - 1; j >= 0; j--)//循环所有位置
{
var g = ~BinarySearch.Find<int>(lcss, pos[j], 0, maxL + 1);
if (g < 0) continue; // lcss中已经有此元素
lcss[g] = pos[j];
maxL = Math.Max(maxL, g);
}
}
}
其中:
lcss = new int[X.Count + 1],lcss[i]记录了长度为 i的 LCS的末尾元素在 X中的最低位置。
lcss[0] = -1;//初始化为最小值 for (int i = 1; i < lcss.Length; i++) lcss[i] = Y.Count;//初始化为最大值
yDic是 Y元素位置的索引:它是一个哈希表, key为 Y中的元素, value保存此元素在 Y中出现位置(从小到大),建立的时候可以采用二分查找插入位置。
BinarySearch.Find<T>:二分查找某元素位置,如果找不到,返回刚好比待查找元素大的元素位置求补。
解法二
从上述解法中可以看出,在循环元素 x在 Y中位置信息的时候存在浪费——先从高位计算,低位置可能覆盖高位计算结果。
特别是在 Y中存在大量重复元素的时候,程序效率会受到比较大的影响。
那么,我们可以做如下改进:从低位置和高位置两头同时计算,如果发现两者拟更 lcss改同一个位置,那么以低位置为准,并跳出循环——因为以后的计算都将针对这个位置,所以没必要计算(想想为什么?)。
另外, yDic[x]中已经使用了的位置信息也不需要再进行计算。
关键代码如下:
renewDic.Clear(); var xLs = yDic[x];//获取 x在 Y中所有位置信息 for (int h = xLs.First, r = xLs.Last; h <= r; h = xLs.Next(h), r = xLs.Prev(r)) { var lo = ~BinarySearch.Find<int>(lcss, xLs[h], 0, maxL + 1); var hi = lo; if (h != r) hi = ~BinarySearch.Find<int>(lcss, xLs[r], 0, maxL + 1); maxL = Math.Max(maxL, hi); SetRenew(lo, h);//设置一个更新 if (lo == hi) break; SetRenew(hi, r); //设置一个更新 } foreach (var kvp in renewDic) RenewLcss(kvp.Key, xLs, kvp.Value, Y);//最后一并更新
其中:
xLs中保存了元素 x在 Y中的位置信息(从小到大),xLs.Next和 xLs.Prev方法可以跳过已经使的位置信息。
如果边循环边更新 lcss将导致后续位置更新出错,所以这里先保存所有更新信息,最后一并更新
—— renewDic是更新 lcss的信息哈希表, key = 在 lcss中要更新的位置, value = 待更新值在 xLs中的位置。
讨论
此算法实现了接近 O(mlogn)的时间复杂度, O(m+n)的空间复杂度,适用于比较长的( >10)的 LCS求解情况——对于长度上百万的
两个字符串,求解时间都能够控制在秒级。
然而因为需要建立索引,对于比较短的( <10)的 LCS求解情况(如英文字典应用),也可以采用动态规划的改进算法,如:时间复
杂度为 O(p(m-p)),空间复杂度为 O(m+n)的算法( p是 LCS长度)。
作者:Silver,原文链接:http://gpww.blog.163.com/blog/static/118268164200996114744301/