后缀自动机学习总结

  后缀自动机是一个有限状态自动机,有限状态自动机的功能是识别字符串。后缀自动机,可以识别一个字符串的所有子串。

  后缀自动机原理。我们考虑如果把一个字符串的后缀建立一棵字典树,那么其状态和结点都是O(N^2)级别的。因为不能充分利用字符串本身的特点。我们考虑,假设我们有一个字符串T,某个串s是其的子串,那么我们在s后面加入一些字符就有可能使其变成T的后缀,而如果串s不是T的子串,就没有必要浪费空间。所以,为了识别所有的后缀,就要尽可能的利用这些可能。后缀自动机是最简状态自动机,其状态数是线性的,可以证明。

  我们按照一定的顺序来一点一点剖分自动机。

  状态。我们知道,后缀自动机是可以在线构建的。每插入一个新的字符,在自动机中就会产生一个新的状态。我们用State(s)表示已经插入字符串s后所能达到的状态。每个状态里面我们有这样3个元素。 pre,其指向上一个可以接收相同后缀的结点,我们不能把这个理解成为指向当前状态的父亲,因为一个节点可能有许多个“父亲”。 len,表示从根节点(空状态,即为没有插入任何一个字符)走到当前点最多要走多少步,也可以理解为在当前状态下自动机可以识别的最长后缀的长度。next[26],记录s已经加入自动机后,再加入一个字符后,该字符在自动机中的位置。

  可以识别。我们说一个状态State可以识别某个字符串x,其意思表示为,在自动机中存在从根结点到当前的状态的一条路径,使得串x是这条路径形成的字符串的一个子串。

  Right集合,我们定义这样一个集合:其表示某一状态下所有能识别的后缀的右端点位置集合。

  举个例子,比如说对于这样一个串T = “aabaaabaaabbab”。可以识别“ab”的的Right集合 S1 = {3,7,11,13}。

  在继续向下之前,先提出一个注意的地方,就是,当我们达到State(s)时,假设是第一次达到,那么此时的自动机是串s的自动机,而不是整个文本串T(s是其的一个子串)的自动机。一定要注意语句的主语。

  Parent树。我们先来对其进行一个如下的分析。

  首先对于一个空串来说,其可识别的位置为所有位置(其实没有必要,这么说是为了一会的图画方便些)。

  Right(空) = {1,2,3,4,5,6,7,8,9,10,11,12,13,14};

  然后我们考虑长度为1的。 

  Right(“a”)={1,2,4,5,6,8,9,10,13},

  Right("b") = {3, 7, 11, 12, 14}.

  接下来, Right(“aa”) = {2, 5, 6, 9, 10}, Right(“ab”) = {3, 7, 11, 14}, Right("bb") = {12}, Right("ba") = {4,8,13}

  Right(“aaa”) = {6, 10}    Right(“aab”) = {3, 7, 11}  Right("aba") = {4, 8}  Right("baa") = {5, 9} Right("bba") = {13} Right("bab") = {14} Right("abb") = {12}

  Right("aaab") = {7, 11}  Right(“aaba”) = {4, 8} Right("abaa") = {5, 9} Right("baaa") = {6, 10} Right("aabb") = {12} Right("abba") = {13} Right("bbab") = {14}

  再向下由于人类智慧太不好搞了,就不画了。但是,根据现有的Right集合,或许我们可以发现些什么。给出一张图来直观地展示:

  我们用小学生看图找特点的思维来一点点看这张图。用罗列的方法来找特点。(嘿嘿,其实 是我的思路比较乱)

  1、首先要记住一件事:树的深度不等于根到这个节点组成的字符串长度。很明显的,有几个单个的叶子结点很好的说明了这个特点。

  2、到某个节点组成的字符串的长度越长,其Right集合里面的元素就越少,也就是说,其在串中的可以匹配的位置就越少。

  3、一个节点的Right集合大小等于以其为根的子树的叶子结点的个数。(这个好像不太明显,因为 我没有把整个树画完QAQ……不过大家可以自行向下扩展)

  4、叶子结点除外,根到一个结点组成的字符串的长度等于其父亲的长度加 + 1,但是我们要清楚的知道,父亲的长度是指其Right集合所表示的所有字符串的最长长度。举个例子来说,对于这样一个串,“AABAABAAB“,Rigth("AB") = {3,6,9} Right(”ABB“) = {3,6,9},他们的Right集合是相同的。所以,用这个也可以来解释 第1 点。树的深度不等于根到这个节点组成 的字符串的长度。

  5、一个非叶结点至少有两个儿子。

  6、两个不同的Right集合,其关系要么是一个是另一个的真子集,要么两个完全不相交。在树上这个是很直观的吧。

我们再来理解一下pre指针的作用。

  某个状态的pre指针指向的结点是当前结点(curRight集合所表示的字符串的最长公共后缀(不是其本身)

  好,下面我们来具体说明一下构建自动机的过程:

  在这个过程中我们需要两个指针,last表示上一次插入的结点位置,cur表示当前的结点位置。

  1、首先是建立一个空结点,表示空串的状态,其len = 0, pre = null;这个是很好理解的吧,在Parent树中,这个结点的角色是树根。

  2、然后假设我们已经构建了前i-1个字符的后缀自动机,现在我们考虑加入第i个字符 x。首先先从last 向 cur连接一条边为x的出边,然后cur.len = last.len + 1,这些都是很显然的吧。

  3、好,现在我们需要确定curpre指针的指向。我们现在在Parent树上沿着lastpre指针向上跳。

  如果不小心跳到了NULL,说明当前自动机中没有x这个字符,那么很好处理,直接把curpre指针指向root,注意是root,而不是NULL

  如果没有跳到NULL,那么,如果last->pre没有x这样一条出边。那么这情况好说,直接从last->precur连一条为x的出边。为什么要这么连?我们考虑,我们已知last->pre指向的是当前状态Right集合所表示的所有字符串的最长公共后缀,我们在当前状态的字符串后加上了一个新的字符x,那就相当于在所有的后缀后面都加了一个x,但是为了保证不重复不遗漏的在后缀上加上这个字符x,所以我们要选择最长的那个来添加,这样自然就用到了这个pre指针, 跳到最长后缀的地方。

  如果last->pre有这样一条出边。那么我们就要分两种情况来讨论:

  我们用p来代表last->pre,用q来代表last->prex出边。

  情况1. 如果q.len == p.len + 1,那么就把curpre设为q

  情况2. 如果q.len > p.len + 1,那么就要把拷贝一个新的结点。

  情况一情况二的解释如下:(来自王梦迪(NOI2015金牌得主)的博客)

  第二种情况——当我们进入一个已存在的转移(p,q)时。这意味着我们试图向字符串中添加字符x+c(其中x是字符串s的某一后缀,长度为len(p)),且该字符串先前已经被加入了自动机(即,字符串x+c已经作为子串包含在字符串s中)。因为我们假设字符串s的自动机已被正确构建,我们并不应该添加新的转移。然而,cur的后缀链接指向哪里有一定复杂性。我们需要将后缀链接指向一个长度恰好和x+c相等的状态,即,该状态的len值必须等于len(p)+1.但这样一种情况可能并不存在:在此情况下我们必须添加一个“分割的”状态。

  因此,一种可能的情形是,转移(p,q)变得连续,即,len(q)=len(p)+1.在这种情况下,事情变得简单,不必再进行任何分割,我们只需要将cur的后缀链接指向q。

另一种更复杂的情况——当转移不连续时,即len(q)>len(p)+1.这意味着状态q不仅仅匹配对我们必须的,长度len(p)+1的子串w+c,它还匹配一个更长的子串。我们不得不新建一个“分割的”状态q:将子串分割成两段,第一段将恰在长度len(p)+1处结束。

   在文章的最后,我们给出完整的模板:

 1 struct State{
 2   int len, pre;
 3   int next[26];
 4 
 5   State(){
 6     len = pre = 0;
 7     memset(next, 0, sizeof next);
 8   }
 9 }st[L<<1];
10 
11 struct SuffixAutomaton{
12   int sz, last;
13 
14   void Init(){
15     last = sz = 0;
16     st[0].len = 0; st[0].pre = -1;
17     sz ++;
18   }
19 
20   void Extend(int c){
21     int cur = sz ++;
22     st[cur].len = st[last].len + 1;
23     int p;
24 
25     for(p = last; p != -1 && !st[p].next[c]; p = st[p].pre)
26       st[p].next[c] = cur;
27 
28     if(p == -1) st[cur].pre = 0;
29     else{
30       int q = st[cur].next[c];
31       if(st[q].len == st[p].len + 1) st[cur].pre = q;
32       else{
33         int cle = sz ++;
34         st[cle].pre = st[q].pre;
35         st[cle].len = st[p].len + 1;
36         for(int i = 0; i < 26; ++ i) st[cle].next[i] = st[q].next[i];
37         for(; p != -1 && st[p].next[c] == q; p = st[p].pre)
38           st[p].next[c] = cle;
39         st[q].pre = st[cur].pre = cle;
40       }
41     }
42     last = cur;
43   }
44 }SAM;
SuffixAutomaton

 

posted @ 2016-01-16 14:11  漫步者。!~  阅读(330)  评论(0编辑  收藏  举报