广义后缀自动机

1).自动机的介绍

首先我们先来介绍一下什么是自动机,有限状态自动机的功能是识别字符串,令一个自动机A,若他能识别字符串S,就记为A(S)=Ture,否则A(S)=False。

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

令trans(s,ch)表示当前状态是s,在读入字符ch之后,所到达的状态。如果trans(s,ch)这个转移不存在,为了方便,设其为null,同时null只能转移到null。null表示不存在的状态。同时另trans(s,str)表示当前状态是s,在读入str之后,所到达的状态。

trans(s,str):
Cur=s;
For i=0 to length(str)-1
Cur=trans(Cur,str[i]);

Trans(s,str)就是Cur。

那么自动机A能识别的字符串就是所有使得trans(init,x)∈end的字符串x。令其为Reg(A)。从状态s开始能识别的字符串,就是所有使得的字符串x。令其为Reg(s)。

2).后缀自动机模型的建立过程

后缀自动机的定义:给定字符串S,S的后缀自动机suffix automaton(以后简记为SAM)是一个能够识别S的所有后缀的自动机。即SAM(x)=Ture,当且仅当x是S的后缀,后面会说明,后缀自动机也能用来识别所有子串。

最简单的实现:考虑字符串”aabbabd”,我们可以将该字符串的所有后缀插入一个Trie(字典树)中,就像下图那样。那么初始状态就是根,状态转移函数就是这颗树的边,结束状态集合就是所有的叶子。注意到这个结构对于长度为N的串,会有O(N2)的节点。节点数量太多了,怎么办?

最简状态自后缀自动机:顾名思义,就是状态数最少的后缀自动机,在后面可以证明它的大小是线性的,我们先介绍一些性质。假如我们得到了这个最简状态后缀自动机SAM,我们令ST(str)表示trans(init,str)。就是初始状态开始读入字符串str之后,所能到达的状态。

分析:令母串为S,他的后缀的集合为Suf,他的连续子串的集合为Fac,从位置a开始的后缀为Suffix(a),S[l,r)表示S中[l,r)这个区间构成的子串。下标从0开始,对于一个字符串s,如果它不属于Fac,那么ST(s)=null。因为s后面加上任何字符串都不可能是s的后缀了,没有理由浪费空间。同时如果字符串s属于Fac,那么ST(s)≠null。因为s既然是S的子串,就可以在后面加上一些字符使得其变成s的后缀,我们既然要识别所有的后缀,就不能放过这种可能性。我们不能对每个s∈Fac都建立一个状态,因为Fac的大小是O(N2)的。我们考虑ST(a)能识别哪些字符串,即Reg(ST(a))。字符串x能被自动机识别,当且仅当x∈Suf。ST(a)能够识别字符串x,当且仅当ax∈Suf。因为我们已经读入了字符串a了。也就是说ax是s的后缀,那么x也是s的后缀,Reg(ST(a))是一些后缀集合。对于一个状态s,我们唯一关心的是Reg(s)。

如果a在s中的[l,r)位置出现,那么他就能识别s从r开始的后缀。例子:那么如果a在s中的出现位置集合是{[l1,r1),[l2,r2),...,[ln,rn)}那么Reg(ST(a))就是{Suffix(r1),Suffix(r2),...,Suffix(r3)}。令Right(a)={r1,r2,...,rn}那么Reg(ST(a))就完全由Right(a)决定。

对于两个子串a,b∈Fac如果Right(a)=Right(b),那么ST(a)=ST(b)。所以一个状态s,由所有Right集合是Right(s)的字符串组成。不妨令r∈Right(s),那么只要给定一个长度就可以确定子串了。考虑对于一个Right集合,容易证明如果长度l,r合适,那么长度l≤m≤r的m也一定合适。所以合适长度必然是一个区间,不妨令s的区间是[min(s),max(s)]。

关于子串的性质:由于每个子串都必然包含在SAM的某个状态里,那么一个字符串a是S的子串,当且仅当,ST(s)≠null,那么我们就可以用SAM来解决子串判定问题,同时也可以求出这个子串出现个数,就是所在状态Right集合的大小。在一个状态中直接保存Right集合会消耗过多的空间,我们可以发现状态Right就是它Parent树中所有孩子Right集合的并集,进一步的话,就是Parent树中它所有后代中叶子节点的Right集合的并集。那么如果按dfs序排序,一个状态的Right集合就是一段连续的区间中的叶子节点的Right集合的并集,那么我们也就可以快速求出一个子串的所有出现的位置了。树的dfs序列:所有子树中节点组成一个区间。

线性构造算法:我们的构造算法是Online(在线)的,也就是从左到右逐个添加字符串中的字符,依次构造SAM,这个算法实现相比后缀树来说要简单很多,尽管可能不是非常好理解。让我们先回顾一下性质:

定义和性质的回顾:状态s,转移trans,初始状态init,结束状态集合end,母串S,S的后缀自动机SAM(Suffix Automaton的缩写),Right(str)表示str在母串S中所有出现的结束位置集合。

一个状态s表示的所有子串Right集合相同,为Right(s)。

Parent(s)表示使得Right(s)是Right(x)的真子集,并且是Right(x)的大小最小的状态x。

Parent函数可以表示一个树形结构,叫他Parent树。

一个Right集合和一个长度,定义了一个子串。

对于状态s,使得Right(s)合法的子串长度是一个区间,为[Min(s),Max(s)]。

Max(Parent(s)) = Min(s)-1。

SMA的状态数量和边的数量,都是O(N)的。

trans(s,ch)==null表示从s出发没有标号为ch的边。

考虑一个状态s,他的Right(s)={r1,r2,...,rn},假如有一条s→t标号为c的边,考虑r的Right集合,由于多了一个字符,s的Right集合中,只有S[ri]=c的符合要求。那么t的Right集合就是{ri+1|S[ri]=c }。

那么如果s出发有标号为x的边,那么Parent(s)出发必然也有。

同时令f=Parent(s),Right(trans(s,c))∈Right(trans(f,c))。

有一个很显然的推论是Max(t)>Max(s)。

我们每次添加一个字符,并且更新当前的SAM使得他成为包含这个新字符的SAM令当前的字符串为T,新字符为x,令T的长度为L,SAM(T)→SAM(Tx)

那么我们新增加了一些子串,他们都是串Tx的后缀,Tx的后缀,就是T的后面添加一个x,那么我们考虑所有表示T的后缀(也就是Right集合中包含L)的节点v1,v2,v3,...

由于必然存在一个Right(p)={L}的节点p(ST(T))。那么v1,v2,...,vk,由于Right集合都含有L,那么他们在Parent树中必然全是p的祖先。可以使用Parent函数得到他们,同时我们添加一个字符x后,另np表示ST(Tx),则Right(np)={L+1}。不妨让他们的后代到祖先排位v1=p,v2,...,vk=root,考虑其中一个v的Right集合={r1,r2,...,rn=L},那么在他的后面添加一个新字符x的话,形成新的状态nv的话,只有S[ri]=x的ri那些是符合要求的。

同时在之前我们知道,如果从v出发没有标号为x的边(我么你先不看rn),那v的Right集合内就没有满足这个要求的ri,

那么由于v1,v2,v3,...的Right集合逐渐扩大,如果vi出发有标号为x的边,那么vi+1出发也肯定有。

对于出发没有标号为x的边的v,他的Right集合内只有rn是满足要求的,所以根据之前提到的转移规则,让它连一条到np标号为x的边。

令vp为v1,v2,...,vk中第一有标号为x的变得状态。

考虑vp的Right集合={r1,r2,...,rn},令trans(vp,x)=q,那么q的Right集合就是{ri+1|S[ri]=x}的集合(注意到这是更新之前的情况,所以rn是不算的)。

注意:我们不一定能直接在q的Right集合中插入L+1。

接下来考虑节点nq,在转移的过程中,结束位置L+1是不起作用的,所以trans(nq)就跟原来的trans(q)是一样的,拷贝即可。

接下来,如果已经存在节点nq,我们还得处理。

回忆:v1,v2,...,vk 是多有Right集合包含{L}的节点按后代到祖先排序,其中vp 是第一个由标号为x的边的祖先。x是这轮新加入的字符。

由于vp,...,vk 都有标号为x的边,并且到达的点的Right集合,随着出发点Right集合的变大,也会变大,那么只有一段vp,...,ve ,通过标号为x的边,原来是到节点q的。(q=Trans(vp,x))。

那么由于在这里q节点已经被替换成了nq,我们只要把vp,...,ve 的Trans(*,x)设为nq即可。

每个阶段:回顾:

令当前串为T,加入字符为x.

令p=ST(T),Right(p)={Length(T)}的字点

新建np=ST(Tx),Right(np)={Length(T)+1}的节点。

对p的所有没有标号x的边的祖先v,trans(x,v)=np

找到p的第一个祖先vp ,它有标号x的边,如果没有这样的vp,那么

Parent(p)=root,结束该阶段。

令q=trans(vp,x),

若Max(q)=Max(vp)+1令Parent=(np)=q,结束该阶段。

否则新建节点nq,trans(nq,*)=trans(q,*)

Parent(nq) = Parent(q) (先前的)

Parent(q) = nq

Parent(np)=nq

对所有trans(v,x) == q的p的祖先v,trans(v,x) 改成nq。

图解:(来自:http://hi.baidu.com/myidea/item/142c5cd45901a51820e25039

我们要将字符串”aabbab”建立成后缀自动机模型。

1.起始只有一个空节点root。

2.现在添加字符’a’,即字符串”a”的后缀自动机为:

3.再添加字符’a’,即字符串”aa”的后缀自动机为:

4.再添加字符’b’,即字符串”aab”的后缀自动机为:

5.再添加字符’b’,即字符串”aabb”的后缀自动机为:

(错误!)

所有结束点的parent树中点都是结束态,所以结点3是结束态,所以此图的”ab”也是后缀,但”ab”并不是”aabb”的后缀,我们还需要一个点辅助,所以我们要这么建立:

(正确!)

找到0(S)时,发现转移"b"已经有节点占了,所以新建节点5,将3号所有信息Copy(包括Parent),然后更新len值,就是node[5]->len=node[5]->parent->len+1,所以5号节点可以代表后缀空串(0号代表的串)+字符"b"=后缀"b",节点3成了中间态,所以将节点为原接受态的节点指向3的转移改为指向5,这时,我们发现指向3的原接受态节点一定是当前节点0(S)及当前未访问的原接受态节点,所以可以直接沿着Parent往上更新,然后节点5的Parent及祖先加入了现在的接受态,再次重申一点:一个节点及其父辈的代表的串有相同的后缀,且代表串长度递减,由于5号节点是接受态,所以他的父辈也是接受态,同时反过来也一样,与任意接受态拥有相同后缀的长度小于当前节点的未访问节点一定是当前节点的父辈,如与5号节点有相同后缀的长度小于5号节点的未访问的节点一定是5号的父辈,一定可以作为接受态。因此为了维护这个性质,我们应该将3号节点的父亲重定义为5,到这里基本上应该明白了。

再分别插入’a’,’b’的后缀自动机为:

广义后缀自动机:

传统后缀自动机是解决单个主串的匹配问题,广义后缀自动机可以用来解决多个主串的匹配问题。

如何将多个主串构建成广义后缀自动机?先将一个主串建立成后缀自动机,让后将重置last,令last=root,下一个字符串再从头节点开始建立,下一状态如果不存在,则以后缀自动机的规则进行建立新节点。

如果下一状态已经建立,我们直接转移到该状态即可,既然到达该状态,说明已经匹配成功的字符串的所有后缀都在该状态及该状态的父节点,父节点的父节点...直到root,所以我们需要对该状态以及他的父节点,他的父节点的父节点。。。直到root进行内容更新,更新的内容当然因题目而异,如果求某个字符串出现的个数,就cnt++,如果求某个字符串出现的位置,就将位置存在结点的一个数组里。

举例如下:(给n个长度为100的字符串,有m个长度为K的目标字符串,问每个目标字符串在主串中哪些位置出现过。)

通用性很强,如果目标字符串是任意长度,就把此代码所有用到K的地方删去;如果求目标字符串的个数,就在结构体中加一个计数cnt,然后将下方xx.push_back()改成cnt++;如果主串长度任意,可以用if(last==root&&weizhi!=0)来记录行数。其他用处于后缀自动机相同,网上也有好多例子与题解,在此不多叙述~。~

优点:再也不用将多个主串连起来建立后缀自动机啦~!空间复杂度大大降低~代码难度不高,就是理解有点复杂。

#include <cstdio>
#include <cstring>
#include <iostream>
#include<vector>
using namespace std;
int weizhi,n,K,m;/*全局变量wezhi方便传递此时坐标,K代表K值,m代表有m个k-mer index*/
int f[30];/*分别将对A,C,G,T映射到0,1,2,3,可以大量减少单个结点的开销*/
struct Complex{/*定义节点*/
    Complex* tranc[4],*father;/*通过father和tranc来链接Parent树,tranc进行对下一个状态询问*/
    int len;/*代表次状态Right的MAX*/
    vector<int> ans;/*存储答案*/
}*last,*root,none,*nil=&none;

Complex *NewComplex(int _)/*申请新节点*/
{
    Complex *C=new Complex;
    C->len = _;
    fill(C->tranc,C->tranc + 4,nil);
    C->father = nil;
    return C;
}

void init()/*初始化*/
{
    f['A'-'A']=0,f['C'-'A']=1,f['G'-'A']=2,f['T'-'A']=3;/*对转移函数f的初始化*/
    root = last = NewComplex(0);/*建立节点root,初始化last*/
}

inline void Add(int c)/*对广义SAM中此时last后面添加状态c*/
{
    Complex *np,*p = last;
    if(last->tranc[f[c]]==nil){
        np= NewComplex(last->len + 1);
        for(; p != nil && p->tranc[f[c]] == nil; p = p->father)  p->tranc[f[c]] = np;
        if(p == nil)    np->father = root;
        else {
            Complex *q = p->tranc[f[c]];
            if(q->len == p->len + 1)  np->father = q;
            else {
                Complex *nq = NewComplex(p->len + 1);
                memcpy(nq->tranc,q->tranc,sizeof(nq->tranc));
                nq->father = q->father;
                np->father = q->father = nq;
                if(nq->len>=K&&nq->father->len<K){
                    nq->ans=q->ans;
                }
                for(;p!=nil&&p->tranc[f[c]]==q;p=p->father)    p->tranc[f[c]] = nq;
            }
        }
    }
    else{
        np=last->tranc[f[c]];
    }
    last = np;
    for(; np != root; np = np->father){
        if(np->len<K) break;
        if(np->father->len<K){
            np->ans.push_back(weizhi);
        }
    }
}

inline int Ask(char s[])/*插入新字符*/
{
    Complex *now = root;
    int length = strlen(s);
    for(int i = 0; i < length; ++i)
        if(now->tranc[f[s[i] - 'A']] != nil)
            now = now->tranc[f[s[i] - 'A']];
        else    return 0;
    int p=0;
    for(int i=0;i<now->ans.size();i++){
        if(now->ans[i]%100>=length-1){
            printf("NO.%d:\tX:%d\t,Y:%d\n",p+1,now->ans[i]/100+1,now->ans[i]%100+2-K);/*此处输出anser,请在此处修改输出格式。*/
        }
        else
            p--;
        p++;
    }
    return now->ans.size();
}
void scan_DNA(){
    for(int j=0;j<n;j++){
        for(int i=0;i<100;i++){
            char c;
            if(c=getchar(),c==EOF) break;
            if(c>='A'&&c<'Z'){
                Add(c - 'A');weizhi++;
            }
            else{
                i--;
                if(last!=root&&weizhi%100){
                    weizhi+=100-weizhi%100;
                }
                last=root;
            }
        }
    }

}
void scan_k_mer_print(){

    //printf("%lld",(long long)sizeof(Complex)); /*结点消耗内存测试*/

}
int k(){
    int k;
    printf("please input n and K:\n");
    scanf("%d%d",&n,&k);
    return k;
}
void scan_m_s(){
    fclose(stdin);
    freopen("CON","r",stdin);
    printf("Please input m:\n");
    scanf("%d",&m);
    char ss[105];
    for(int i = 0; i < m; ++i) {
        printf("Please input the %dth lenght is %d k-mer index:\n",i+1,K);
        scanf("%s",ss);
	printf("%s",ss);
        int last_ans = Ask(ss);
        printf("ALL:%d\n",last_ans);
    }
}

void autoscan_m_s(){
    scanf("%d",&m);
    char ss[105];
    for(int i = 0; i < m; ++i) {
        scanf("%s",ss);
	printf("%s",ss);
        int last_ans = Ask(ss);
        printf("ALL:%d\n",last_ans);
    }
}
int main(){
    K=k();/*读入K值*/
    freopen("f1out.txt","r",stdin);/*测试数据文件*/
	/*数据输出文件*/
    init();/*初始化*/
    scan_DNA();/*读入DNA序列并建立广义后缀自动机模型*/
    printf("The establishment of success!\n");
    //freopen("out_2.txt","w",stdout);/*想要将答案输出文件请取消此行注释*/
    /*想要控制台输入k-mer index请选择scan_s();并且注释掉autoscan_m_s();*/
    /*想要文件输入k-mer index,请选择autoscan_m_s();并且注释掉scan_m_s();*/
    scan_m_s();/*手动输入*/
    //autoscan_m_s();/*文件读入,注意:请保证文件格式正确!*/
    return 0;
}

压缩后缀自动机:

答题思路就是将没有支链的一小段节点压缩成一个节点,这个压缩节点的父节点的next应记录此压缩节点中的第一个结点的状态。

因为一直没有写,实现就交给你们啦~!

谢谢CLJ大神的PPT指导!谢谢小彭玉、YYN、叉姐的指导!谢谢建模让我深入思考!





时间: 2024-08-01 06:59:32

广义后缀自动机的相关文章

【BZOJ3926】[Zjoi2015]诸神眷顾的幻想乡 广义后缀自动机

[BZOJ3926][Zjoi2015]诸神眷顾的幻想乡 Description 幽香是全幻想乡里最受人欢迎的萌妹子,这天,是幽香的2600岁生日,无数幽香的粉丝到了幽香家门前的太阳花田上来为幽香庆祝生日. 粉丝们非常热情,自发组织表演了一系列节目给幽香看.幽香当然也非常高兴啦. 这时幽香发现了一件非常有趣的事情,太阳花田有n块空地.在过去,幽香为了方便,在这n块空地之间修建了n-1条边将它们连通起来.也就是说,这n块空地形成了一个树的结构. 有n个粉丝们来到了太阳花田上.为了表达对幽香生日的祝

E. Three strings 广义后缀自动机

http://codeforces.com/problemset/problem/452/E 多个主串的模型. 建立一个广义后缀自动机,可以dp出每个状态的endpos集合大小.同时也维护一个R[]表示那个串出现过. 所以可以算出每个状态的dp[i][k]表示第k个串在第i个状态中出现的次数. 可以知道sigma dp[i][0...k]是等于  endpos集合的大小. 然后把这个贡献加到min(i)....max(i)中去就可以了 差分一下. #include <bits/stdc++.h>

BZOJ 3277 串 (广义后缀自动机)

3277: 串 Time Limit: 10 Sec Memory Limit: 128 MB Submit: 309 Solved: 118 [Submit][Status][Discuss] Description 字符串是oi界常考的问题.现在给定你n个字符串,询问每个字符串有多少子串(不包括空串)是所有n个字符串中至少k个字符串的子串(注意包括本身). Input 第一行两个整数n,k. 接下来n行每行一个字符串. Output 输出一行n个整数,第i个整数表示第i个字符串的答案. Sa

hdu 5853 Jong Hyok and String(广义后缀自动机)

题目链接:hdu 5853 Jong Hyok and String 题意: 给你n个字符串,m个询问,每次询问一个字符串 定义set(s)={(i,j)} 表示 s在第i个字符串中出现,且末尾位置为j. 对于一个询问,求set(Qi)=set(t) ,t串的数量. 题解: 如果是n=1,那么就是后缀自动机的一道裸题,答案就是Qi串匹配的最后一个节点x,ml[x]-ml[f[x]]. 现在是多个串,那么就建立一个广义后缀自动机.每次插入一个串后,将last=root,然后继续插下一个就行了. 最

【BZOJ2806】【CTSC2012】Cheat 广义后缀自动机+二分+Dp

题目 题目在这里 思路&做法 我们先对标准作文库建广义后缀自动机. 然后对于每一篇阿米巴的作文, 我们首先把放到广义后缀自动机跑一遍, 对于每一个位置, 记录公共子串的长度\((\)即代码和下文中的\(val\)数组\()\) 接着我们二分答案, 用DP检验. Dp方程很好想, \(d_i = max \{ d_j + i - j \ | \ i-val_i <= j <= i-lim \}\) 可以用单点队列优化. 代码 #include <iostream> #incl

【codeforces666E】Forensic Examination 广义后缀自动机+树上倍增+线段树合并

题目描述 给出 $S$ 串和 $m$ 个 $T_i$ 串,$q$ 次询问,每次询问给出 $l$ .$r$ .$x$ .$y$ ,求 $S_{x...y}$ 在 $T_l,T_{l+1},...,T_r$ 中的哪一个里出现次数最多,输出出现次数最多的串编号(如果有多个则输出编号最小的)以及相应出现次数. $|S|,q\le 5\times 10^5$ ,$\sum\limits_{i=1}^m|T_i|\le 5\times 10^4$ . 题解 广义后缀自动机+树上倍增+线段树合并 对 $S$

BZOJ 3926: [Zjoi2015]诸神眷顾的幻想乡 广义后缀自动机 后缀自动机 字符串

https://www.lydsy.com/JudgeOnline/problem.php?id=3926 广义后缀自动机是一种可以处理好多字符串的一种数据结构(不像后缀自动机只有处理一到两种的时候比较方便). 后缀自动机可以说是一种存子串的缩小点数的trie树,广义后缀自动机就是更改了一下塞点的方式让它可以塞多个子串. 1 #include<iostream> 2 #include<cstdio> 3 #include<algorithm> 4 #include<

广义后缀自动机模板

后缀自动机能解决很多单串的问题.但是一旦到了多串的情况.可能就会变得有些棘手 这个时候你可能会想能不能将多个串一起构建出和单串后缀自动机那样子拥有诸多优美性质的自动机呢? 答案当然是有的.那就是广义后缀自动机 对于广义后缀自动机.和普通的后缀自动机写法上有些许不同之处 大致就是在插入新串的时候.需要把当前状态指针 last 指回 root 还有一个问题.网上的人们都说广义后缀自动机在新插入节点的时候要判是否已经存在 这个就造成了代码的迥异 关于代码.借鉴了这个博客 ==> Click here

bzoj 4566 找相同字符 —— 广义后缀自动机

题目:https://www.lydsy.com/JudgeOnline/problem.php?id=4566 建出两个串的广义后缀自动机: 统计每个点在两个串中出现次数的子树和,其实就是在两个串中的 right 集合大小: 然后统计答案乘起来即可. 代码如下: #include<cstdio> #include<cstring> #include<algorithm> using namespace std; typedef long long ll; int co