与小郁合作之——求最长公共子串

参考:

 

http://en.wikipedia.org/wiki/Suffix_tree

http://marknelson.us/1996/08/01/suffix-trees/

http://en.wikipedia.org/wiki/Generalised_suffix_tree

http://en.wikipedia.org/wiki/Longest_common_substring_problem

 

与小郁合作的第一个课题是求多个字符串的最长公共子串,这一算法一般在生物学上用于基因比对,以从中发现物体与物体之间的最大相似,目前效率最高的算法是与suffix-tree相关的,这个可以参考文中开头的参考部分。我们的第一个任务是根据字符串的suffix-tree,构造算法写出多个字符串的lcs(最短公共子串),我设计了以下思路:

 

(1) 构造第一个字符串的suffix-tree

 

(2) (1)构造的树的基础上继续构造第二颗树的suffix-tree,当每次遇到字符串的某尾时,我们就在叶结点里加入字符串的信息。

 

(3) 不断重复步骤(2)直到所有的字符串构造完,这样,如果这颗树的某个叶结点代表的后缀是某个字符串的子串,这个叶结点就含有了该字符串的信息,同时包括该后缀子串是从这个字符串什么位置开始的。

   

(4) 通过递归调用遍历该颗后缀树,可以求出所有结点的相关信息,对于叶结点来说,这些信息本身通过步骤(2)后已经有了,对于那些不是叶结点的结点来说,他们本身代表后缀的前缀(从根结点到他们结点上走一遍形成的字符串是某些字符串的前缀),与这些结点相关的信息来自于他所有的孩子。例如,叶结点

    A包含字符串(135)的信息,即该叶结点代表的后缀是字符串135的子串,叶结点B包含字符串(2,4,6)的信息,即该叶结点代表的后缀是字符串246的子串,如果结点C仅有A B两个孩子,则结点C包含(1,2,3,4,5,6)这些字符串的信息。遍历的过程可以计算出每个结点相关的字符串的数目,那些数目达到m

    (字符串个数)的结点所代表的即为公共子串。

   

(5) 第二次遍历该后缀树,可以找出lcs

 

假设字符串个数为m,而所有字符串的长度之和为n的时候,这个算法原则上可以达到O(n)级,因为构造后缀树花掉O(n)时间,遍历树花掉O(mn)时间,由于m<<n,可以视为常数,在实现该算法的过程中,我发现当要求的字符串达到一定数量,字符串达到一定长度的时候,内存就不够用了,很显然,由于构造的整颗树信息都在内存之中,如果要处理大批量的数据,这无疑是该算法的一个瓶颈,同时,如果考虑到用外部存储,算法另外要改造,还要加上内外存数据交换,这个问题就变得非常复杂了。

 

对于在很多情况下,物体与物体的相似可能我们只需要一个近似值,是不是有更好的办法呢。德国的老板提供给小郁这样的一种思路,看下面的图:

 

算法举例

 

0110001

11001010

001100110

 

 

 

 

0

 

 

 

 

 

1

 

 

 

(1,1) (1,4) (1,5) (1,6) (2,3) (2,4) (2,6) (2,8)

(3,1) (3,2) (3,5) (3,6) (3,9)

(1,2) (1,3) (1,7) (2,1) (2,2) (2,5) (2,7)

(3,3) (3,4) (3,7) (3,8)

 

00

 

 

10

 

 

 

01

 

 

11

 

(1,4) (1,5) (2,3) (3,1) (3,5)

(1,3) (2,2) (2,5)(2,7) (3,4) (3,8)

(1,1) (1,6) (2,4) (2,6) (3,2) (3,6)

(1,2) (2,1)

(3,3) (3,7)

000

 

100

 

010

 

110

001

 

101

011

 

111

(1,4)

删除

(1,3) (2,2) (3,4)

(2,3)删除

(1,2) (2,1) (3,3) (3,7)

(1,5) (2,3) (3,1) (3,5)

(2,5) 删除

(1,1) (3,2) (3,6)

删除

 

0100

1100

 

 

0110

1110

0001

1001

 

1011

0011

 

 

删除

(1,2) (2,1) (3,3)

 

 

(1,1)

(3,2) (3,6) 删除

删除

(1,4)

删除

(2,2)

(3,4) 删除

 

删除

(3,5) 删除

 

01100删除

11100删除

 

 

 

 

 

 

 

 

 

 

上面是求3个字符串的lcs, 为说明简单,我们所列的字符串只包含了01两种字符,表格的第一行列出了只含1个字符的子串,表格的第二行列出了包含对应该字符的所有字符串的位置信息,如(11)表示0字符在第一个字符串的第1个位置出现过。第三行把字符的子串扩展到了两个字符,如在0前加入01形成0010,在1前加入01形成0111,同时,第四行给出了第三行对应子串的位置信息。如此下去,其实可以把子串和他的位置信息看做一个结点,这样,这个表格可以看作一颗树,如0010可以看作0的孩子。000100又可以看作是00的孩子。再看000结点,由于该子串只在字符串1的第4个位置出现过,已经不再是公共子串,所以该结点不再有孩子了。显然,红色标注的1100便是lcs

 

这个算法思想非常简单,而且容易实现,根据以上描述,很显然是一个穷举算法,是不是需要也要在内存中构造整颗树呢?当数据量很大的时候,是否能够找出近似值呢。我和小郁经过多次讨论,最终对算法做了如下设计。

 

首先我们设计了lcslocation类,主要是把所有与输入的所求字符串的信息都放入此类,lcslocation里有三个属性,分别为a b ca里包含所有的输入字符串信息,b里包含所有字符串含有的公共字符,在上面的表中,含有的公共字符就是01,如果第一个字符串含有字符2,而第二个不含有字符2,则2是不会出现在以上表格中的。

 

另外,针对树,我们设计了下面lcstreelcsnode类,lcstree并不是真正上面的树,而是一颗抽象树,重点封装了算法的过程。Lcsnode是上面子串代表的节点的一个抽象。我们采用的仍然是以上穷举遍历的过程,不过,为了能够尽快接近结果,我们并不是进行上述类似的广度遍历。从0010这层节点开始,我们设置了遍历的宽度,假如是3,即我们按照一定的条件对第一层节点生成的所有孩子,并把满足条件的孩子(节点子串仍包含所有字符串的位置信息,仍然是公共子串,否则让它死亡)进行排序(一般是位置信息的丰富程度),挑出3个最有可能生成lcs的节点放入队列,其余的压入栈中,后面的遍历是把所有队列中的节点取出来,让他们生孩子,然后在所有的孩子中挑出最有可能生成lcs的节点入队列,其余入栈,在这个算法的过程中,如果队列中已经没有3个孩子了,就从栈(还可以规定宽度,抛弃一些可能性不大的节点)中挑出来补足。

 

同时,在这个过程中,如果父节点能够生出满足条件的孩子,显然,孩子表示的子串比父结点表示的更长,父节点就没有保存的必要了,我们始终保持lcs指针指向当前最长的公共字串,如果从队列中弹出的节点的字串长度大于当前的lcs,就让lcs指针指向它。

 

该遍历直到栈和队列空为止,或者超过规定的时间。

 

很显然,该算法不需要构造整棵树,同时又能够尽快接近真正的lcs,而且lcs指针始终指向当前lcs,还可以在限定的时间内退出,输入当前的lcs,虽然可能是真正lcs

 

为了能够尽快接近真正的lcs,挑选满足条件的孩子大有文章可做,我们也试验了多种挑选方法,在此,我把数据结构中的锦标赛排序和堆排序都用上了,小郁在我把算法实现后得出了多组测试数据。

 

以下是算法中一些重要数据结构c++描述

 

主要类:

 

//lcs中所有字符串相关信息整理为此类,作为lcs算法的输入数据

 

class LcsLocation{

                     ......

     

      private:

            //lcs中字符串个数

             int strcnt;

 

            //字符串数组,用以存储每个字符串含有的字符

              //(不重复,如"abacd" 对应 "abcd")            

             string *a;

            

             //a数组中所有字符串的公共字符(不重复),若干字符串的lcs中的字符必然出现在b

             string b;

            

             //二位数组,第一维表示某个字符串,第二维表示b中的某个字符,

             //该二位数组指出所有字符在每个字符串中的位置信息;

             set<int> **c;

            

             ......

            

      ......

}

 

//lcs算法的核心类,封装了算法所有核心的数据结构

 

class LcsTree{

      ......

     

      public:

            

             void setChildNumber(int childnumber);

            

             void setLcsLocation(LcsLocation *lcslocation);

            

             //根据lcslocation,构建初始化的树,生成树的根节点,

             //与该根节点关联的是所有的公共字符的位置信息(从lcslocationc中获得)

             //同时把该根节点入队列

             void init(LcsLocation &lcslocation);

            

             //根据type从栈或队列中取出一个lcsnode并生成相关的子node

             //在此过程中如果从栈或队列中取出的lcsnodelevel值比当前lcslevel大,即当前lcs指向取出的lcsnode

             void genechildnode(int type);

            

             //求公共字串的核心过程

             //遍历该树(即根据栈或者队列中的节点信息,生成它的子节点并进行相关处理)

             void tranverse();

            

             //遍历是否结束,即是否栈和队列中已无可处理的节点,此时表示整棵lcstree树已经遍历完成

             bool isfinTran();

            

             LcsNode *getLcs();

            

             ......

 

      private:

             //存放当前的lcs,遍历结束后,即为最长公共字串

             LcsNode *lcs;

            

             //与树相关的队列与栈,暂存还没有生成子节点的节点

             LcsQueue<LcsNode> lcsqueue;

             LcsStack<LcsNode> lcsstack;

            

             //含有公共字符的个数(即lcslocation中的b的字符个数)

             int childnumber;

             int originallevel;

             LcsLocation *lcslocation;

            

             //暂存树遍历过程中的中间节点

             vector<LcsNode *> childvect;

            

             ......

           

      ......

            

};

 

//lcstree中的节点

 

class LcsNode {

 

      ......

     

                     public:

                           //节点层号

                           int level;

                                         

                           //节点中的公共字符串

                           string lcsstr;

                                         

                            //是否终结点,所谓的终结点是该节点代表的字串已被确认为非公共字串

                           bool isendnode;

                           bool ischoose;

                                         

                           //该节点附带的与节点中字符串相关的位置信息

                           set<locinfo, loccompare> *nodeinfoptr;

                                         

                            //做相应标记(在对暂存于向量数组中的lcsnode按照某种条件排序后执行)

                           void setChoose(int vecwidth, int lcswidth, bool value);

                           ......

                      

                     ......

};

 

算法的思路是这样的:

 

void lcsmain() {

                                  

                                   根据输入得到lcslocation;

                                   lcstree.init(lcslocation);//初始化lcstree

                                   start = time(NULL);

                                   while (!lcstree.isfinTran() && duration < USE_TIME) {//遍历lcstree直到遍历完毕或者超时

                                                        lcstree.tranverse();

                                                        finish = time(NULL);

                                                        duration = (double) finish - start;

                                   }                                

                                   //输出求得的lcs信息

                                   lcstree.outputLcsNode(*lcstree.getLcs());

}

 

    在这个实践课题的许多环节中,德国教授和小郁多次讨论算法的改进过程,并对设计测试时的一些细节进行了点评,非常严谨地指出了很多实现细节中的关键问题,实非目前大多国内大学的教授所能比,让我感到由衷地敬佩。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值