开源中文网

您的位置: 首页 > 编程开发 > C++语言编程 > 正文

数据结构:后缀自动机 WJMZBMR讲稿的整理和注释

来源: cnblogs  作者: R.E.Chicherina

  链接放在这里,有点难理解,至少我个人是的。

  后缀自动机是一种有限状态自动机,其功能是识别字符串是否是母串的后缀。它能解决的问题当然不仅仅是判断是不是后缀这种事,跟字符串的连续子串有关的问题都可以往这个方面考虑,毕竟字符串的每个连续字串都可以看做是两个长度不同的后缀去掉他们的公共部分得到的。

  自动机由五个部分组成:alpha:字符集,state:状态集合,init:初始状态集合,end:结束状态集合,trans:状态转移函数。

  定义trans(s,ch)为当前状态为s,读入字符ch后所达到的状态。若不存在此转移,则将转移的结果定义为null,表示不存在的状态。自动机A能识别的字符串就是所有使得trans(init,x)∈end的字符串,令这些字符串组成的集合为Reg(A)。另外,对于自动机中的某一状态s,从s开始能识别的字符串记为Reg(s)。

  考虑字符串“aabbabd”的后缀,一共有7个,简单的想法是可以将这7个后缀构造成一个trie树(ppt里的图好像有问题,多了abbd这条路线),缺点是状态数太多,对于长度为N的字符串,其节点的规模会达到O(N^2),而后缀自动机相比起来就小多了,其状态数是线性的。

  根据aabbabd构造的后缀自动机:

   

  几个关键的概念:

  ST(str):从初始状态init读入字符串str后转移到的状态。

  S:母串。

  Suf:S的后缀集合。

  Fac:S的所有连续子串集合。

  Suffix(a):从位置a开始的集合。

  S[l,r):S中[l,r)这个区间构成的连续子串。

  Right:对于一个在母串中出现过的连续子串,其每次出现的结束位置集合成为该子串的Right集。例如aabbabd中,子串ab出现了2次,其Right集为{3,6}。

  state:后缀自动机的状态,对应一个Right集,一个状态由所有Right集相同的子串构成。对于一个字符串,其连续子串的规模是O(N^2)。在所有的连续子串中,有这样一些子串,它们的Right集合是相同的。仍然以aabbabd为例:子串aabb,abb,bb在字符串中均只出现了一次,Right集均为{4},同时Right集合为{4}的只有这3个子串,这3个子串构成一个状态,含义是只在5这里出现了一次的子串。

  结论:

  1.令r∈Right(s),只要给定子串的长度len就可以确定子串了。

  2.对于某一个状态s,如果长度为l和r的子串都属于此状态,那么位于l和r之间的所有子串也都属于此状态。

  长度在l和r之间的子串都可以看做长度为r的子串的后缀,长度为r的子串每一次出现,那么长度[l,r)也就是更短一些的子串也必定出现了。随着子串的长度从r逐渐减小,子串出现的机会越来越多,当然也可以在一定范围内保持不变。如果到l为止仍然属于同一个状态,能么可以想见[l,r]全部都属于同一个状态。

  那么,状态可以这样简单地理解,如图(长方形代表字符串......):

  一个状态由多个构造相同的分支组成,每个分支包括若干个字符串,这些字符串由前半部分横线阴影所代表的每各个缀加上后边斜线阴影的全部内容构成。对应上一段的内容,横线阴影左边界到斜线阴影右边界的长度相当于r,横线阴影右边界到斜线阴影右边界的长度相当于l。

  令状态s的长度区间为[Min(s),Max(s)]。

  3.状态数和边数是线性的。

  考虑两个状态a,b。他们的Right集合分别为Ra和Rb。若Ra和Rb有交集,假设r∈Ra∩Rb。因为状态是按照Right集合来划分的,所以不同的状态手下的字符串也是不相同的。现在对于r这个位置来说,状态a的字符串和状态b的字符串都在这里出现并结束了,所以,他们至少结尾的一部分是相同的。而a和b手下的字符串是没有交集的,只有一种可能:它们开始的位置各不相同,也就是说,必然满足[Min(a),Max(a)]和[Min(b),Max(b)]是没有交集的。这说明,a和b中的某一个,它手下全部的字符串都是另一个手下任意一个字符串的后缀,哪怕是最短的那个。那么,对于Ra和Rb来说,其中一个是另一个的真子集。

  总结一下两种状态之间的关系:或者不相交,或者一个是另一个的真子集,可以想见,各个状态的Right集合实际上构成了一个树形结构,不妨称为parent树。

  假想一下,每个字符串结尾有一个空字符,对于只有一个空字符的字符串,它的Right集合就是{1,2,3,4......N},这个状态就是根节点。在字符串左边填入不同的字符,相当于增加对字符串的限定,也可以理解为对字符串的分类,a+str是一类,b+str是一类......每一类对应一个儿子节点,也是一个状态,继承了父节点Right集合的一个子集。当然,有的时候,在左边加一个字符并不能分类,连续加上Max(s)-Min(s)个才行,因为当前字符串左边只能加上那个字符,否则在母串中就不存在了。

  那么,树形结构,叶节点的数量为N,总的节点数最多为2*N。

  构造算法:

 

  首先定义状态节点:

struct Node {
    int step;
    Node *pre, *nxt[26];
    void clear() {
        step = 0;
        pre = NULL;
        memset(nxt, NULL, sizeof(nxt));
    }
}*root, *last;

  step表示此状态手下的字符串的最大长度,pre指向此状态在parent树中的父亲结点。nxt[i]数组指向由当前状态后接一个字符i转移到的状态。这里并没有保存Right集合,因为每个状态节点的Right集合是其后代所有叶子节点的Right集合的并集。root表示初始状态,last表示结束状态。

  每次添加一个字符,并维护当前的数据结构,使之保持我们所希望的性质。

  令当前的字符串为T,新字符为x,T的长度为L。添加了x后新增加了一些子串,他们都是Tx的后缀,而Tx的后缀就是在T的后缀后面添加了一个x。此时需要做的就是将这些字符串分发给不同的状态节点,如果需要的话,可能会新建一个状态节点。

  添加了这个x后,首先新建一个状态:

    Node *np = cur++, *p = last;
    np->clear();
    np->step = p->step + 1;

  其含义是只在这个新的结尾出现了一次的字符串。

  考虑T所有的后缀(Right集合中包含L)所在的状态v1,v2......显然这些状态是parent树中从树叶到树根的一条路径。设v1=p是其中的叶节点,也就是只包含了一个最长的后缀的状态。

  考虑其中一个v的Right集合{r1,r2,......rn=L}在它的后面添加一个x形成新的状态的话,只有S[ri]=x的那些ri是符合要求的。如果从v出发没有标号为x的转移(先不看rn),那么v的Right集合内就没有满足这个要求的ri。由于v1,v2......的Right集合逐渐扩大,所以如果从vi出发有标号为x的转移,那么之后的状态全部都有这样的转移。对于出发没有标号为x的转移v,它的Right集合内只有rn是满足要求的,符合状态np的“只在新的结尾出现了一次”,所以根据转移规则,让它连一条到np标号为x的转移:

    while (p && !p->nxt[w]) {
        p->nxt[w] = np, p = p->pre;
    }

  循环结束有两种可能:

  1:这些后缀所在的状态中没有任何一个有标号为x的转移,这种情况直接设置np的父节点为root就可以结束了。

    if (p == NULL) {
        np->pre = root;
    }

  2:循环到某个p时,该状态有标号为x的转移。此时,又有两个分支。设q为trans(p,x)得到的状态。如果Max(q)==Max(p)+1,就可以直接将np作为q的儿子。

        Node *q = p->nxt[w];
        if (q->step == p->step + 1) {
            np->pre = q;
        }

  但Max(q)==Max(p)+1并不一定成立,这里我想了很久才想明白。考虑这样一个字符串:

a a b b c c x a a b b c c x b b c c x
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

  最后一个x是刚刚添加的字符。在检查到bbcc这个后缀时,其Right集为Rp={6,13,18}。显然这种状态是有标号为x的转移的,转移后的结果是Rq={7,14},但其Max为7。而最后新添加的这个x,显然并不具备aabbccx的结构,否则就成了上面的那种情况。此时不能直接插入,而是需要新建一个状态,Right集为{7,14,19},Max为5。

            Node *nq = cur++;
            memcpy(nq->nxt, q->nxt, sizeof(q->nxt));
            nq->pre = q->pre;
            nq->step = p->step + 1;
            np->pre = q->pre = nq;

  nq就是新建的状态,基本照搬原来的q的数据,修改的地方:Max,应该的。设置q的父节点为nq,双方都具有尾部的bbccx结构,之前的q在这个基础上多了个aa,所以q是nq的子状态,然后设置np的父节点为nq。

  接下来,向p的祖先方向找,凡是满足p->nxt[w] == q的都将其的父节点改为nq。

            while (p && p->nxt[w] == q) {
                p->nxt[w] = nq, p = p->pre;
            }

  最后,修改结束状态为np

    last = np;

  完整代码:

#define N 5050
struct Node {
    int step;
    Node *pre, *nxt[26];
    void clear() {
        step = 0;
        pre = NULL;
        memset(nxt, NULL, sizeof(nxt));
    }
}*root, *last;
Node pool[N * 2], *cur;
void init() {
    cur = pool;
    root = last = cur++;
    root->clear();
}
void extend(int w) {
    Node *np = cur++, *p = last;
    np->clear();
    np->step = p->step + 1;
    while (p && !p->nxt[w]) {
        p->nxt[w] = np, p = p->pre;
    }
    if (p == NULL) {
        np->pre = root;
    } else {
        Node *q = p->nxt[w];
        if (q->step == p->step + 1) {
            np->pre = q;
        } else {
            Node *nq = cur++;
            memcpy(nq->nxt, q->nxt, sizeof(q->nxt));
            nq->pre = q->pre;
            nq->step = p->step + 1;
            np->pre = q->pre = nq;
            while (p && p->nxt[w] == q) {
                p->nxt[w] = nq, p = p->pre;
            }
        }
    }
    last = np;
}

  一些相关的练习题:HDU5470 3518 4416 5343 5853 4436 4622 5558  4641 1403

Tags:自动机 讲稿 数据结构
关于开源中文网 - 联系我们 - 广告服务 - 网站地图 - 版权声明