字尾自動機入門

2022-09-14 14:27:09 字數 4021 閱讀 5179

\(|s|\) 表示字串 \(s\) 的長度。

字尾用 \(\mathrm(s,i=\mathrm)\) 表示,如果不寫\(i\)則表示字尾集合。

\(\sigma\) 表示字符集大小

對於乙個字串 \(s\) ,對於任意乙個 \(p|s\) ,定義乙個集合 \(\mathrm(p)\)(以下簡稱 \(\mathrm(p)\) ),表示這個子串在 \(s\) 中所有出現的位置,用 \(p\) 中最後乙個字母的位置進行記錄。

比如 \(s="aabab"\) 中,\(\mathrm("a")=\, \mathrm("ab")=\\)

我們將 \(\mathrm\) 相同的子串稱為乙個類。

引理 1

\[\mathrm(s_1)\subset \mathrm(s_2)\longleftrightarrow s_2\in\mathrm(s_1)

\]這個引理比較顯然,就不證明了

引理 2

\[\mathrm(s_1)\wedge \mathrm(s_2)\ne\empty

,|s_1|<|s_2|

\longrightarrow

\mathrm(s_2)\subset \mathrm(s_1),s_1\in\mathrm(s_2)

\]如果較小的字串不是較大的字串的字尾,那麼這兩個字串中至少有乙個位置的字元不相同,但是這兩個不同字元出現在相同的原串位置,顯然有問題,因此 \(s_1\) 一定是 \(s_2\) 的字尾,再根據 \(引理1\) ,可以推出 \(\mathrm(s_2)\subset \mathrm(s_1)\)

可以發現,兩個 \(\mathrm\) 要麼為包含關係,要麼沒有交。

引理 3

乙個類中的子串按長度從小到大排,長度一定連續。並且所有子串,除去最大的子串,都是最大子串的字尾。

根據 \(引理1\) ,容易證明第二句話。

設該類中最長,最短的子串分別為 \(s_,s_\) 。

根據 \(引理2\) ,對於所有 \(s\in\mathrm(s_),|s_|<|s|<|s_|\) ,可以發現 \(\mathrm(s_)\subset\mathrm(s)\subset\mathrm(s_)\)

因為 \(\mathrm(s_)=\mathrm(s_)\),那麼 \(\mathrm(s_)=\mathrm(s)=\mathrm(s_)\)。

所以對於所有滿足條件的 \(s\) ,都一定在這個類中。

根據 \(\mathrm\) 的定義和性質(只有包含和不交),可以根據其包含關係構建出一棵樹,乙個節點表示乙個類,乙個節點和任意乙個兒子的連線,表示乙個包含關係。根節點是空子串,顯然它的 \(\mathrm=[1,|s|]\)

這棵樹能夠表示所有子串。

這棵樹上的節點集合就是 \(\mathrm\) 上的節點集合。

我們用 \(\mathrm(u)\) 表示 \(\mathrm\) 中節點 \(u\) 的父親,用 \(\mathrm(u)\) 表示節點 \(u\) 能夠代表的最長的字串,用 \(\mathrm(u)\) 表示 \(|\mathrm(u)|\)。

顯然乙個節點代表的所有字串長度不會重複,且從小到大排序,恰好是 \(\mathrm(\mathrm(u))+1~\mathrm(u)\),所以乙個節點代表的字串一共有 \(\mathrm(u)-\mathrm(\mathrm(u))\) 個

可以發現,\(\mathrm\) 中,從父親節點走向兒子節點,可以表示乙個字串在開頭新增字串。乙個節點的所有祖先所代表的字串,都是這個節點的字尾(廢話)

在 \(\mathrm\) 的基礎上,我們對每乙個節點設立轉移邊,規模為 \(o(\sigma)\) ,若乙個節點 \(u\) 通過代表字元 \(c\) 轉移邊到達另乙個節點 \(v\) ,則表示 \(u\) 中某乙個字串在末尾加上 \(c\) 就可以變成 \(v\) 中的字串。(這一點可以聯絡 \(\mathrm自動機\) 思考)

首先確定方案:動態加入每乙個字元,同時更新字尾自動機。

用原串表示加入新字元之前的字串,用新串表示加入新字元之後的字串。

考慮加入乙個字元 \(c\) ,實際上就是將所有原串的字尾都加上 \(c\) ,也就是說所有原串的字尾都可以使用轉移邊轉移到新串的字尾。

現在給出模版**,慢慢解釋:

struct samnode  nd[maxn];

//lst:原串最長的字尾(原串自己)所在的節點

void sam_init()

void sam_insert(int c)

}}

sam_init()不講了,大家都懂。

np表示新加入的節點。

nd[np].len = nd[p].len + 1;

for (; p && !nd[p].ch[c]; p = nd[p].fa) nd[p].ch[c] = np;

首先就是總長度+1。

接著是乙個有點意思的迴圈。第乙個迴圈條件使p只能到達根節點(廢話)。接著第二個限制條件,如果節點nd[p]中沒有字串能轉移到nd[np],增加轉移邊。如果找到乙個節點存在轉移邊 \(c\) ,就說明已經存在乙個新串的字尾,那麼比這個字尾長度小的字尾都存在了,退出。(注意,這裡找到的時候原串存在的最長的新串字尾去除新加入的字元 \(c\) )

if (!p) nd[np].fa = 1;
若一直跳到根節點,表示原串中沒有任何子串是新串的字尾,那麼新加入的節點就代表的所有新串的字尾,然後將這個節點設成根節點的兒子。

else \) 是 \(e\) ,那麼在原串中不存在的所有字尾的 \(\mathrm\) 都是 \(e\) 的子集,沒有任何問題,直接將所有不存在字尾所在的類(這個類中 \(\mathrm}\) ,因為是新出現的字尾)設成\(v\)的兒子。

else

如果 \(v\) 中不僅包含最長的已存在字尾,而且包含其他奇奇怪怪的子串(最長已存在子串是他們任何乙個的字尾,但他們不是原串中不存在的新串字尾的字尾),那麼不能將 \(np\) 直接設成 \(v\) 的兒子。

這時候將 \(v\) 分成兩部分,一部分只包含最長已存在字尾,設為 \(nv\) ,另一部分包含奇奇怪怪的子串,保留在 \(v\) 中,顯然此時 \(v\) 的父親應該是 \(nv\) ,而且 \(np\) 的父親也應該是 \(nv\)。

接著將原本轉移到\(v\)的代表\(c\)的轉移邊都放到\(nv\)。

根據構建過程,可以發現節點規模是 \(o(n)\) 的,若將 \(\sigma\) 看成常數,那麼轉移邊的數量也是 \(o(n)\)。

字尾自動機中每個節點包含了出現位置相同完全的一類字串,而且乙個節點中的字串互為字尾。轉移邊表示乙個節點中的某乙個字串新增乙個字元可以成為另乙個節點中的某乙個字串。\(\mathrm\) 中從父親走向兒子,實際上表示在在一類字串中,在前頭加入若干個字元。

idea 1

由於 \(\mathrm\) 是乙個 \(\mathrm\) ,而且由於其每個節點代表的字串集合不會交,所有可以考慮一下dp。設 \(f(u)\) 表示從節點 \(u\) 出發進行轉移可以得到的字串總數(包括空串)。

顯然可以得到

\[f(u)=1 + \sum\limits_ f(v)

\]**就不寫了。

idea 2

首先每乙個節點代表的字串絕對和其他節點的字串沒有乙個是相同的,而每乙個節點代表的不相同的字串共有 \(\mathrm(u)-\mathrm(\mathrm(u))\) 個。

所以最終的答案為:

\[\sum\limits_ (\mathrm(u)-\mathrm(\mathrm(u)))

\]首先乙個字串\(s\)的出現次數其實就是 \(|\mathrm(s)|\),所以問題轉化為求出每乙個節點的 \(\mathrm\) 。

可以在 \(\mathrm\) 上做樹形dp。

設節點 \(u\) 的 \(\mathrm\) 為 \(f(u)\)。

首先原串中每乙個字首所在的節點 \(u\) 都設為 \(f(u)=1\) ,接著進行轉移。

\[f(\mathrm(u))+=f(u)

\]

字尾自動機

基礎知識 step i 表示的是字串i在原字串中的位置。pareint i 表示root到parent i 的子串是root到i的最長字尾。字尾自動機遍歷可以得到原字串的所有子串。特殊技巧 一 字尾自動機的不同子串數有兩種求法 1.ans step i step parent i 1 i cnt 2...

字尾自動機

常用於處理字串問題,可以高效解決許多字串問題。有點像將乙個字串的所有字尾都建在乙個ac自動機上,但不同的是字尾自動機的節點數最多為2 n,因為它只記錄需要記錄的點,一些沒有記錄東西的點可以視為與下面有價值的節點並在一起,這樣大大降低了時間複雜度和空間複雜度。對於每乙個節點記錄它的後面加上每個字元後字...

字尾自動機

基礎學習 簡潔明瞭的講解 總狀態數不超過2n 12n 1 2n 1 包括初始狀態 統計每個end po sendpos endpos 等價類出現位置數量時,要按長度從長到短的計算cnt cntcn t。那為什麼一定要從長到短呢?比如回文自動機就直接是按照節點編號從大到小計算cnt cntcn t 罪...