连载3:最长公共子序列

问题

给定两个序列
X = { x1 , x2 , ... , xm } Y = { y1 , y2 , ... , yn }求 X和 Y的一个最长公共子序列(LCS, Longest Common Sequence)

分析

一般典型的解法是利用动态规划,利用如下性质:

  1. 若xm = yn ,则 zk = xm = yn,且 Z[k-1] 是 X[m-1] 和 Y[n-1] 的最长公共子序列
  2. 若xm != yn ,且 zk != xm , 则 Z 是 X[m-1]和 Y 的最长公共子序列
  3. 若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,每级计算过程如下表:

 

那么计算过程可以解释为:

  1. 先从X中取出第一个元素 4,通过 yDic发现 Y中包含 4,位置为 3,6,那么长度为 1的公共子序列,末尾元素在 Y中最低位置为 3;
  2. 从X中取出第二个元素 2,通过 yDic发现 Y中包含 2,位置为 1,5,那么长度为 1的公共子序列,末尾元素在 Y中最低位置更新为 1。长度为 2的公共子序列,末尾元素在 Y中最低位置为 5;
  3. 以此类推,遇到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/

其他文章:
连载1:卡特兰数(Catalan)
连载2:序列 ABAB对应字符串集合

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值