學習參考
如果你看完這些,你後綴自動機基本上沒問題了
如果你想會用的話那隻要看前4個就可以了(因爲博主自己也只看了前四個的一部分......)
下面是我自己的小結
先是一些後綴自動機的定義
ch[x][s]表示的是節點x(也可以說是狀態)字符s的節點編號。返回該結點對應的子串加上某個字符後生成的合法子串在後綴自動機中所對應的位置(其實就和字母樹一樣),如果該指針不存在,就說明這樣的子串是不存在的(即不是s的子串)
fa[x]其實就是parent指針,但是注意這裏的fa[x]表示的並不是他的父親是誰,因爲上面也說過了一個節點因爲代表了多個含義所以他也有多個父親。所以parent指針指向的位置表示的是如果當前節點可以接某個後綴那麼parent指針指向的節點也一定可以接。即上一個可以接受後綴的節點。
right(s)表示的子串s在原串中所有出現位置的右端點的集合。對於right滿足right(s)是right(fa(s))的子集。right的求法,按照parent樹中深度從大到小,依次將每個狀態的right集合併入他fa狀態的right集合。
l[x]表示的是從根節點到該節點的最大距離,對於一個狀態s,他代表的串的長度區間就是(len[fa],len[x]].
再引進一些不錯的性質:
①從root到任意結點p的每條路徑上的字符組成的字符串,都是當前串t的子串.
②因爲滿足性質一,所以如果當前結點p是可以接收新後綴的結點,那麼從root到任意結點p的每條路徑上的字符組成的字符串,都是必定是當前串t的後綴.
③如果結點p可以接收新的後綴,那麼p的fa指向的結點也可以接收後綴,反過來就不行.
然後是一些構造的時候後面會用到的性質
1、每個狀態i的點表示的字符串長度的範圍是(len[fa[i]]…len[i]]。(從len[fa[i]]+1…len[i])
2、每個狀態i表示的所有字符串的出現次數和right集合都是一樣的。
3、由fa構成的數叫做parent樹,parent樹上子節點的right是父節點的子集。
4、後綴自動機的parent樹是原串的反向前綴樹,那麼也是原串的反串的後綴樹。
5、兩個串的最長公共後綴是在後綴自動機上對應的狀態在parent樹上的lca的狀態。
我主要說一下的是後綴自動機插入的時候len[q]==len[p]+1和len[q]>len[p]+1的情況
增量法。我們對於每個狀態s,記他代表的最長子串的長度爲l(就是上面提到的l[x])
考慮我們當前已經有了前}s|-1個字符的後綴自動機,且現在的自動機中[1,|s|-1]處於end狀態(就是終結狀態)
現在加入第|s|個字符(設爲c),我們令新建了的狀態爲np,顯然l[np]=|s|
然後考慮如何轉移:我們加入的是一個last->np的轉移,我們也應該加入一個fa(last)->np的轉移,直到我們發現到達某個狀態已經有一個字符c的轉移爲止。不妨設這個狀態是p,設他經過字符c的轉移後的狀態爲q
如果最終到達root,那麼直接將fa[np]=root
如果不爲根節點root,就分以下兩種情況討論。
1.len[q]=len[p]+1.這就是說明p,q直接相連中間不夾雜其他的字符,那麼q代表的所有串的right集合相同,那我們我們將fa[np]=q即刻。爲什麼要這麼做呢?這樣是可以節省空間和狀態的,這麼連接之後q點就代表了他之前的狀態以及現在新加入的狀態。
len[q]=len[p]+1直接加點解釋:這樣構造np就不需要代表所有1到np位置的後綴了,只需要代表(len[fa(np)],len[np] ]長度的後綴
2.len[q]>len[p]+1.這種情況下q代表的串總,長度不超過len[p]+1的串的right集合會多出一個值{s|,而長度超過它的串這不會,之所以會出現len[q]>len[p]+1的情況就是因爲p,q之間可能還存在其他的字符,而我們先加入的狀態是不滿足的。那麼爲了維護一個狀態中所有串的right相同這一性質,我們需要新建了一個狀態nq,nq代表的是原來q代表的串中所有長度不超過len[p]+1的串,因此len[nq]=len[p]+1,nq的其他屬性(fa和轉移)和原來的q點一致,同時建立新的fa指針fa[q]=fa[np]=nq,也就是用nq代表了q,np兩個狀態。
然後我們再次從p開始:本來p的c字符轉移到的點是q,現在它轉移到nq。同理fa[q]的c字符也要轉移到nq,直到當前點的c字符轉移到的不是q爲止。
從w->q表示的是長度小於len[w]+1的後綴
len[q]>len[p]+1克隆解釋:對於從p->q這條路得到的後綴的right(即長度小於len[p]+1的後綴)都需要加上|s|這個位置,而大於len[p]+1的不需要,這樣right就不統一了,所以就需要分裂出來
fa[q]=nq解釋:因爲q現在表示(len[p]+1,len[q]]的後綴,nq表示(len[原來fa[q]],len[p]+1]的後綴
後綴自動機構造理解的話,更重要的是他的各種應用,他的用法有很多,並且都比較巧妙,所以還是需要取看一下他各種應用的模板題
最後貼一下代碼
這個是求原串S中出現次數在[A,B]之間的子串的個數
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned int uint;
const int N = 1e5+10;
struct Node {
int f, len, ch[26];
void init() {
len = 0, f = -1;
memset(ch, 0xff, sizeof (ch));
}
};
ll sz[N<<1]; //sz[i]表示狀態i的right(i)的大小,即狀態i表示的字符串出現的次數
struct SAM {
Node e[N<<1];
int idx, last;
void init() {
idx = 0;
last = newnd();
}
int newnd() {
e[idx].init();
return idx++;
}
void add(int c) {
int end = newnd();
int p = last;
e[end].len = e[p].len + 1;
for (; p != -1 && e[p].ch[c] == -1; p = e[p].f) {
e[p].ch[c] = end;
}
if (p == -1) e[end].f = 0;
else {
int nxt = e[p].ch[c];
if (e[p].len + 1 == e[nxt].len) e[end].f = nxt;
else {
int nd = newnd();
e[nd] = e[nxt];
e[nd].len = e[p].len + 1;
e[nxt].f = e[end].f = nd;
for (; p != -1 && e[p].ch[c] == nxt; p = e[p].f) {
e[p].ch[c] = nd;
}
}
}
sz[end]=1;
last = end;
}
};
SAM sam;
char str[N];
int ws[N];
int tp[N<<1];
ll d[N<<1]; //d[i]表示從狀態i出發,不同的子串的數目,即不同的路徑數
int main()
{
int A,B;
while(scanf("%s%d%d",str,&A,&B)!=EOF)
{
sam.init();
int len=strlen(str);
for(int i=0;str[i];i++) sam.add(str[i]-'A'); //插入
//對sam的節點按照len,從小到大排序重新標號,即給定節點的拓撲序
for(int i=0;i<=len;i++) ws[i]=0;
for(int i=1;i<sam.idx;i++) ws[sam.e[i].len]++;
for(int i=1;i<=len;i++) ws[i]+=ws[i-1];
for(int i=sam.idx-1;i>0;i--) tp[ws[sam.e[i].len]--]=i;
//按照拓撲序進行遍歷
//ll res=0;
for(int i=sam.idx-1;i;i--)
{
int v=tp[i];
sz[sam.e[v].f]+=sz[v];
if(sz[v]>=A&&sz[v]<=B) d[v]++;
//if(sz[v]>=A&&sz[v]<=B) res+=sam.e[v].len-sam.e[sam.e[v].f].len;
//我看其他也有直接將len相減來求的,但我不知道爲什麼這個求出來的串不會重複,但是這個方法書是可以A的,直接輸出res就可以了
for(int j=0;j<26;j++)
if(sam.e[v].ch[j]!=-1) d[v]+=d[sam.e[v].ch[j]];
}
ll ans=0;
for(int i=0;i<26;i++)
{
if(sam.e[0].ch[i]!=-1) ans+=d[sam.e[0].ch[i]];
}
for(int i=0;i<sam.idx;i++) sz[i]=0,d[i]=0;
printf("%lld\n",ans);
getchar();
}
}