1. 程式人生 > >字尾自動機概述

字尾自動機概述

如果對字尾自動機有一定了解,這幾篇文章對你可能會有些許幫助:
menci’s字尾自動機學習筆記
字尾自動機學習指南
loj上的字尾自動機講解
一些題目
聽說對拆點講解很詳細
127~132周

以題目為主,當然也有一些講解。下面說一下我對字尾自動機的理解,不給出詳細證明。

字尾自動機的特點

首先,字尾自動機是一種有限狀態自動機,他可以識別且僅識別一個字串的字尾。但是這不併不是字尾自動機強大的地方,我可以說如果把AC自動機反向插入我同樣可以做到這一點。

字尾自動機真正的用處在於:它可以識別一個串的所有子串。

非常優秀的是字尾自動機只會有

O ( n ) 個節點,也就是說在字符集看做常數的情況下,對於字尾自動機的構建可以做到 O ( n )

字尾自動機不同於AC自動機的地方在於:它並不是一棵樹,不看prarent鏈的話,字尾自動機是一張DAG,這就讓字尾自動機每一個節點的意義玄妙了起來。

字尾自動機每一個節點代表什麼?

定義一個子串的所有出現位置結尾的集合為這個子串的right集合。
每一個節點識別的子串right集合相同,right集合相同的子串用同一個點接受。有一個很有意思的性質是right集合相同的字串他們的長度一定是連續的。可以感性理解一下,aba和ba,aba出現的位置ba一定出現過,所以ba出現的位置數一定大於等於aba。

字尾自動機的parent鏈是什麼?

如果B節點的right集合包含A節點right集合的最小集合,那麼A節點有一條parent鏈連向B節點。可以把parent鏈當做fail鏈,但是他們之間有一些微妙的區別。
構造的意義:順著parent鏈跳可以逐漸增大right集合以便找到所有可以轉移的狀態。
匹配的意義:我在匹配時記錄已經匹配的長度,和當前在哪一個節點,那麼我就可以知道我匹配最長是哪一個串,跳parent鏈時通過增大right集合,減小匹配長度,從而找到合法轉移。這個等一會兒詳細講。
parent鏈是一棵樹,它組成了原串反串的一棵字尾樹(這裡不研究字尾樹)。

舉個例子吧:abb

a:1
ab:2
b:2,3
bb:3
abb:3

我們把right集合相同的放在一起:
1,a
2,ab
3,b
4,abb,bb
把子集包含的連上parent鏈(紅色代表parent鏈),這樣我們就得到了一個字尾自動機:
這裡寫圖片描述
我們滿足上面的條件就構造出了一個字尾自動機。
可以證明對於任意串滿足上面的條件都可以構造一個字尾自動機。

如何構造一個字尾自動機?

字尾自動機是一個增量演算法,也就是說已經構造出了 s [ 1 , i ] 的字尾自動機,現在要構造 s [ 1 , i + 1 ] 的字尾自動機。
對於每個節點記錄一下它的轉移,接受的最長串(len)和parent鏈(p),每次構造完之後記錄一下到達的節點在哪兒(las)

加入的時候肯定要有一個節點接受整個串,新建點x的right集合為{i+1},同時賦值len=i+1。
right集合含有i的狀態都可以轉移到新建節點。發現las節點的right集合正好是i,根據上面的性質las節點的parent指向right集合包含i的節點,所以我們順著las的parent鏈可以遍歷所有right集合包含i的節點。

這些點都應該有一個向x的 s [ i + 1 ] 的轉移。

下面分3種情況討論:
1.如果順著parent達到了空節點,那麼所有right集合含有i的節點都增加了向x的一個轉移。到達空節點即可結束。
2.到達了一個本來就有一個 s [ i + 1 ] 轉移的節點y。設y向 s [ i + 1 ] 轉移到q,那麼right集合和q相同的子串已經被q接受,q的right集合是包含i+1最小集合,所以x的parent鏈連向p。
3.但是按照2的做法會有一個問題:
這裡寫圖片描述
這個字尾自動機是錯誤的,可以發現ab,b的right集合並不相同。
那究竟是什麼情況讓我們構造出了這樣一個錯誤的自動機呢?
其實我們要讓自動機新接受abb,bb,b三個串,並把它們分配給對應的節點,我們向前跳發現長度小於等於1的串(也就是b)已經被接受過了。q這個點的right集合裡應該增加一個i+1。於是發現一個問題,q本來不止接受abb的字尾,它還多接受了一個串ab,ab的right集合並沒有改變,但b改變了。本來right集合相同的串變成了不同的,但是我們用一個點接受,就產生了錯誤。
換一種說法,我們讓x接受長度大於y的len+1的串,q接受[?,len+1]的串。但是q並不只接受[?,len+1]的串,q本來接受了一個長度大於len+1的串。這些長度大於len+1並不是y的轉移,所以不是abb的字尾,這些串就不合法。
分情況討論:

y的len+1 = q的len
這樣按2的方法做。

y的len+1 < q的len
我們強行構造一個點讓它接受[?,len+1]的串。我們用q複製一個點nq,nq的所有狀態等於q的狀態。沿著y的parent鏈向上找,把所有向q的轉移轉向nq。這樣nq就接受了[?,len+1]的串的串,剩下的q就接受了[len+2,?]的串。q的parent鏈指向nq,x也指向nq。因為nq的riight集合包含q的和x的,所以把q和x的right集合指向nq。很顯然,nq的len賦值為y的len+1

這樣我們就構造了出了一個字尾自動機。

如何使用這個字尾自動機呢?

首先我們可以知道一個串是否作為模板串的子串出現過,因為這個自動機可以識別模板串的所有子串。這是字尾自動機的一個最簡單的應用。

字尾自動機的強大之處在於:它可以計算每個子串出現次數。

怎麼做?把非複製節點出現次數定為1,這個節點出現一次,它沿parent鏈向上的點都會出現一次。於是我們就可以求parent鏈的拓撲序向上遞推。

每個點會有一個值,代表這個點管理的所有子串出現了那麼多次。並且一個點x管理哪些子串呢?長度為(x的parent的len+1到x的len)。

這樣我們就可以在後綴自動機裡匹配了。就像AC自動機匹配即可。但是有一個問題,走到一個點時並不代表匹配了這個節點管理的所有字串。所以我們需要額外記錄一個表示當前匹配長度的變數。這個變數與x的len取一個較小值就好。

試一試吧:

找相同字元

寫了這道題會對字尾自動機有一個大概理解,這裡不贅述做法。

code:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
struct lxy{
    int to[26],p,len,k,num;
}a[400005];

int cnt=1,las=1,len;
int tax[200005];
int tp[400005];
char s[200005];
long long ans;

void insert(int c,int w){
    a[++cnt].len=w;a[cnt].num=1;
    int i;for(i=las;a[i].to[c]==0&&i!=0;i=a[i].p) a[i].to[c]=cnt;
    las=cnt;
    if(i==0){
      a[cnt].p=1;return;
    }
    int q=a[i].to[c],nq;
    if(a[i].len+1==a[q].len){
      a[cnt].p=q;return;
    }
    nq=cnt+1;for(int j=i;a[j].to[c]==q;j=a[j].p) a[j].to[c]=nq;
    a[nq]=a[q];a[nq].num=0;a[nq].len=a[i].len+1;
    a[q].p=nq;a[cnt].p=nq;las=cnt;cnt++; 
}

void querytp(){
    for(int i=1;i<=cnt;i++) tax[a[i].len]++;
    for(int i=1;i<=len;i++) tax[i]+=tax[i-1];
    for(int i=1;i<=cnt;i++) tp[tax[a[i].len]--]=i;
}

void matchit(int u,int pos,int l){
    a[u].k++;ans-=1ll*a[u].num*(a[u].len-l);
    if(s[pos]==0) return;
    for(;a[u].to[s[pos]-'a']==0&&u!=0;u=a[u].p);
    if(u==0) matchit(1,pos+1,0);
    else matchit(a[u].to[s[pos]-'a'],pos+1,min(l,a[u].len)+1);
}

int main()
{
    scanf("%s",s+1);len=strlen(s+1);
    for(int i=1;i<=len;i++)
      insert(s[i]-'a',i);   
    querytp();
    for(int i=cnt;i>=1;i--) a[a[tp[i]].p].num+=a[tp[i]].num;
    scanf("%s",s+1);
    matchit(1,1,0);
    for(int i=cnt;i>=1;i--) a[a[tp[i]].p].k+=a[tp[i]].k,ans+=1ll*a[tp[i]].k*a[tp[i]].num*(a[tp[i]].len-a[a[tp[i]].p].len);
    printf("%lld",ans);
}

更多的題目可以參見文章開頭的連結。