廣義後綴自動機學習筆記

廣義後綴自動機

Tags:字符串 題解html

做業部落

評論地址


1、前言

廣義後綴自動機實際上考得比普通後綴自動機要更多更靈活
因此這裏做爲一個小專題呈現,題單在後綴自動機的總題單裏
爲了更好掌握廣義\(SAM\),這裏提供一個高級模板題的題解node

2、構建方法

普通後綴自動機處理單串的問題,多串就只能使用廣義\(SAM\)ios

最方便的構建學習

一種方案是把字符串中間依次用從未出現過的字符鏈接,看成一個字符串處理,再多幾個特判spa

for(int i=1;i<=m;i++)
    {
        string A; cin>>A; s[++l]='#';
        for(int j=0,l=A.size();j<l;j++) s[++l]=A[j];
    }

一種方案是每插入一個串就把\(lst=1\),其他什麼都不用改變3d

for(int i=1;i<=m;i++)
    {
        lst=1; cin>>s; l=s.size();
        for(int j=0;j<l;j++) Extend(s[j]-'0');      
    }

最準確的構建code

其實準確來講,廣義後綴自動機是經過遍歷Trie樹建的,因此要先建好\(Trie\)
每次找到\(fa[x]\)的節點做爲\(lst\)日後接便可htm

有兩種方法:\(BFS\)\(DFS\)遍歷建後綴自動機
葉大佬告訴咱們,\(DFS\)會被卡成\(n^2\)\(BFS\)不會,就像下面圖片中的例子PKUSC買了個草稿本~blog

int Extend(int f,int c)
{
    if(ch[f][c]&&len[ch[f][c]]==len[f]+1) return ch[f][c];//?!
    int p=++node,ff=0;lst=p;
    len[p]=len[f]+1;
    while(f&&!ch[f][c]) ch[f][c]=p,f=fa[f];
    if(!f) {fa[p]=1;return p;}
    int x=ch[f][c],y=++node;
    if(len[x]==len[f]+1) {fa[p]=x;node--;return p;}
    if(len[p]==len[f]+1) ff=1;//?!
    memcpy(ch[y],ch[x],sizeof(ch[y]));
    len[y]=len[f]+1; fa[y]=fa[x]; fa[x]=fa[p]=y;
    while(f&&ch[f][c]==x) ch[f][c]=y,f=fa[f];//緣由就在這
    return ff?y:p;
}

你會發現這和後綴自動機的模板有些區別誒
首先是有一個返回值,這個很好理解,返回的是若是下次要日後接的\(lst\),也就是方便找到\(Trie\)樹某點在\(SAM\)上的位置,直接傳進來就好啦
而後是有一個if(len[p]==len[f]+1) return y;的特判
前面也有一個if(ch[f][c]&&len[ch[f][c]]==len[f]+1) return ch[f][c];的特判圖片

好,廣義\(SAM\)的構建就講完了
什麼?!WTF?!那個特判是啥意思我還不造嘞!
別急,咱們來經過例題理解它

3、例題

連接HN省隊集訓
題意:給\(n\)個字符串\(s\),強制在線進行\(4\)個操做,詢問/串長\(1e5\)\(n\le20\)
PFyaKH.png
題解
對於操做\(1\)
記錄\(pos[i][j]\)表示在第\(i\)次操做後\(j\)號串的結尾字符在\(SAM\)上的節點編號是\(j\),當在第\(i\)次操做在字符串\(x\)後插入字符時,令\(lst=pos[i-1][x]\),而後照常插入便可

對於操做\(2\)
記錄\(siz[i][j]\)表示在\(i\)號點,貢獻給\(j\)號字符串的\(Endpos\)集合的大小,實際意義就是\(i\)號點表明的字符串集合在\(n\)個串中共出現了\(\sum_{j=1}^{n}siz[i][j]\),其中在\(j\)號串中出現了\(siz[i][j]\)次。
查詢\(z\)串中\(x\)串的出現次數,那麼\(x\)串在\(y\)次操做後的結尾位置是\(pos[y][x]\),那麼\(siz[pos[y][x]][z]\)就是答案。
這裏的\(siz\)是在\(parent\)樹上對子樹進行求和纔有上述意義,原理可見於個人另外一篇博文後綴自動機,可是這裏須要在線插入節點,因而咱們用\(lct\)維護\(parent\)樹上的\(siz\),每次斷開或者連上父親的時候至關是給節點到\(parent\)根的路徑\(+/-1\),因此這裏的\(lct\)只須要支持鏈加,不須要改變樹的根因此沒有\(makeroot\)\(reverse\)等操做,若是須要學習\(lct\),可見個人另外一篇博文lct

對於操做\(3\)
維護一個全局變量\(sum\),每一個節點貢獻的本質不一樣的子串個數就是\(len[x]-len[fa[x]]\)

對於操做\(4\)
\(SAM\)上匹配到T的結束位置的節點,\(Ans=max(siz[pos][i]),i=1..n\)

複雜度分析
操做\(1\)的總複雜度是\(\sum len[s_i]×log(n)×20\)的,操做\(2\)\(log(n)\)的,操做\(3\)\(O(1)\),操做\(4\)\(\sum len[T_i]+20\)的,\(log\)都是\(lct\)的複雜度
因此總共時間複雜度是\(O(\sum len[s_i]*log(n)+\sum len[T_i])\)
空間複雜度\(O(\sum|S|*logn*20)\)左右

代碼以下

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#define ll long long
using namespace std;
int read()
{
    char ch=getchar();int h=0,t=1;
    while((ch>'9'||ch<'0')&&ch!='-') ch=getchar();
    if(ch=='-') t=-1,ch=getchar();
    while(ch>='0'&&ch<='9') h=h*10+ch-'0',ch=getchar();
    return h*t;
}
const int N=1e6+100;
char s[N];
int n,zx,m,ans,node=1;
int fa[N],ch[N][11],len[N],pos[N][21];
ll sum;
namespace lct
{
    #define lc t[x].ch[0]
    #define rc t[x].ch[1]
    struct LCT{int fa,ch[2],val[21],tag[21];}t[N];
    int isrt(int x) {return t[t[x].fa].ch[0]!=x&&t[t[x].fa].ch[1]!=x;}
    void Tag(int x,int y,int k) {t[x].val[y]+=k; t[x].tag[y]+=k;}
    void pushdown(int x)
    {
        for(int i=1;i<=n;i++)
        {
            if(!t[x].tag[i]) continue;
            if(lc) Tag(lc,i,t[x].tag[i]);
            if(rc) Tag(rc,i,t[x].tag[i]);
            t[x].tag[i]=0;
        }
    }
    void rotate(int x)
    {
        int y=t[x].fa,z=t[y].fa;
        int k=t[y].ch[1]==x;
        if(!isrt(y)) t[z].ch[t[z].ch[1]==y]=x; t[x].fa=z;
        t[y].ch[k]=t[x].ch[k^1];               t[t[x].ch[k^1]].fa=y;
        t[x].ch[k^1]=y;                        t[y].fa=x;       
    }
    void Push(int x){if(!isrt(x)) Push(t[x].fa);pushdown(x);}
    void splay(int x)
    {
        Push(x);
        while(!isrt(x))
        {
            int y=t[x].fa,z=t[y].fa;
            if(!isrt(y)) (t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
            rotate(x);
        }
    }
    void Access(int x) {for(int y=0;x;y=x,x=t[x].fa) splay(x),rc=y;}
    void Add(int x,int y,int op) {for(int i=1;i<=n;i++) Tag(x,i,t[y].val[i]*op);}
    void link(int x,int y) {Push(x);t[x].fa=y;Access(y);splay(y);Add(y,x,1);}
    void cut(int x) {Access(x);splay(x);Add(lc,x,-1);lc=t[lc].fa=0;}
    int query(int x,int k) {Push(x);return t[x].val[k];}
}
int Extend(int f,int id,int c)
{
    if(ch[f][c]&&len[ch[f][c]]==len[f]+1)
    {
        int p=ch[f][c];
        lct::Access(p);lct::splay(p);lct::Tag(p,id,1);
        return p;
    }
    int p=++node; len[p]=len[f]+1; lct::t[p].val[id]=1;
    while(f&&!ch[f][c]) ch[f][c]=p,f=fa[f];
    if(!f) {fa[p]=1;lct::link(p,1);sum+=len[p]-len[fa[p]];return p;}
    int x=ch[f][c];
    if(len[f]+1==len[x]) {fa[p]=x;lct::link(p,x);sum+=len[p]-len[fa[p]];return p;}
    if(len[f]+1==len[p])//!!
    {
        lct::cut(x);lct::link(p,fa[x]);lct::link(x,p);
        memcpy(ch[p],ch[x],sizeof(ch[p]));
        fa[p]=fa[x]; fa[x]=p;
        sum-=len[p]-len[fa[p]];
        while(f&&ch[f][c]==x) ch[f][c]=p,f=fa[f];
    }
    else
    {
        int y=++node; len[y]=len[f]+1;
        lct::cut(x);lct::link(y,fa[x]);lct::link(x,y);lct::link(p,y);
        memcpy(ch[y],ch[x],sizeof(ch[y]));
        fa[y]=fa[x]; fa[x]=fa[p]=y;
        while(f&&ch[f][c]==x) ch[f][c]=y,f=fa[f];
    }
    sum+=len[p]-len[fa[p]];
    return p;
}
int calc()
{
    int x=1,res=0,i,l; scanf("%s",s+1);
    for(i=1,l=strlen(s+1);i<=l&&x;i++) x=ch[x][s[i]-'0'];
    if(i!=l+1) return 0;
    lct::Push(x);
    for(i=1;i<=n;i++) res=max(res,lct::t[x].val[i]);
    return res;
}
int main()
{
    n=read();zx=read();
    for(int i=1;i<=n;i++)
    {
        scanf("%s",s+1); pos[0][i]=1;
        for(int p=1,l=strlen(s+1);p<=l;p++)
            pos[0][i]=Extend(pos[0][i],i,s[p]-'0');
    }
    m=read();
    for(int i=1;i<=m;i++)
    {
        int op=read(),x,y,z;
        for(int p=1;p<=n;p++) pos[i][p]=pos[i-1][p];
        if(op<=2) x=read(),y=read();
        if(op==1) y=(y^(ans*zx))%10,pos[i][x]=Extend(pos[i][x],x,y);
        else if(op==2) z=read(),printf("%d\n",ans=lct::query(pos[y][x],z));
        else if(op==3) printf("%lld\n",sum);
        else printf("%d\n",ans=calc());
    }
    return 0;
}

Wait!
出題人和我講這道題的時候,特別強調要特判!
可是我一直不理解,把特判if(len[f]+1==len[p]) ff=1;刪了後仍是過了本題
因而他給了一組\(hackdata\)而且在\(BZOJ\)增強了數據

Input
2 0
3201
01
1
2 2 0 1

Output
Right : 1
Wrong : 0

首先咱們建出第一個串的\(SAM\)(黑邊是\(SAM\)上的邊,基佬紫邊是\(Parent\)樹上的邊)
PF65OH.jpg
咱們先加特判,那麼繼續建就會這樣(姨媽紅邊是刪去的邊)
PF646e.jpg
繼續加邊
PF6T0A.jpg
可是若是不加這個特判,就會撒夫夫地把藍\(4\)給複製一邊,可是這時第二個串的\(0\)的結尾位置是\(5\),當我查詢\(2\)串在\(1\)串中出現次數就會訪問到\(siz[pos[x][2]][1]\),此時很遺憾地發現調用到了\(5\)節點,它沒有記錄藍\(4\)的信息
PF6omd.jpg
這樣寫代碼很長,有沒有一種簡單的寫法捏?
有的,咱們發現黑\(5\)不會再被訪問到,至關於他的貢獻被算到了六號點上,同時又沒有點指向它因此它不會被訪問到。對比上面兩張圖,咱們發現其實就是\(圖1\)中的黑\(5\)\(圖3\)中的黑\(6\)是徹底同樣的誒!
因而直接返回黑\(6\)就好啦
把上述代碼的\(Extend\)改爲下面這種

int Extend(int f,int id,int c)
{
    if(ch[f][c]&&len[ch[f][c]]==len[f]+1)
    {
        int p=ch[f][c];
        lct::Access(p);lct::splay(p);lct::Tag(p,id,1);
        return p;
    }
    int p=++node,ff=0; len[p]=len[f]+1; lct::t[p].val[id]=1;
    while(f&&!ch[f][c]) ch[f][c]=p,f=fa[f];
    if(!f) {fa[p]=1;lct::link(p,1);sum+=len[p]-len[fa[p]];return p;}
    int x=ch[f][c];
    if(len[f]+1==len[x]) {fa[p]=x;lct::link(p,x);sum+=len[p]-len[fa[p]];return p;}
    if(len[f]+1==len[p]) ff=1;
    int y=++node; len[y]=len[f]+1;
    lct::cut(x);lct::link(y,fa[x]);lct::link(x,y);lct::link(p,y);
    memcpy(ch[y],ch[x],sizeof(ch[y]));
    fa[y]=fa[x]; fa[x]=fa[p]=y;
    while(f&&ch[f][c]==x) ch[f][c]=y,f=fa[f];
    sum+=len[p]-len[fa[p]];
    return ff?y:p;
}

是否是很方便?

從本質上分析這兩個特判

if(ch[f][c]&&len[ch[f][c]]==len[f]+1) return ch[f][c];
咱們建\(SAM\)是在\(Trie\)樹上遍歷建的,這句話表示訪問到\(Trie\)樹上一個點,它在\(SAM\)上已經被建出來了,因此不須要新建
if(len[f]+1==len[p]) ff=1;
這句特判起做用的條件是中間\(f\)沒有被改變,也就是說存在\(ch[f][c]\),假設\(f\)表示的字符串是\(A\),那麼插入字符\(c\)後產生\(Ac\),可是\(Ac\)已經存在於自動機上,因此要把它從原來的狀態剝離出來,複製一遍,從後來的操做中能夠發現最後咱們須要的\(lst\)也就是\(return\)的點應該是複製出來的那個點

相關文章
相關標籤/搜索