〇、關於后綴自動雞的一些牢騷廢話和引入
它取名叫后綴自動雞,但是實際上在一個自動雞出爐之后好像和后綴基本上沒什么關系,按照 \(\text{JZM}\) 大佬的話,叫 “子串自動雞” 可能更恰當.
只不過,它取名叫后綴自動雞,原因是之一可能是在原本的串 \(S\) 后面插入新字符 \(c\) 時,將 \(S\) 的許多后綴遍歷了一遍,因而得名 “后綴自動雞”.
但是實際上,它在建成之后,自動雞中似乎並沒有彰顯 “后綴” 的東西,而更像是把串 \(S\) 的所有子串放到一個時空復雜度都很優秀的 \(DAG\) 中.
哦,還有句話,后綴自動雞的簡稱 \(\text{SAM/Suffix Automaton}\),而不是 \(\text{SAC/Suffix Autochiken}\).
壹、一些新概念英語
在進行正式說明前,我們有一些新概念需要理清.
一、終止節點等價類(\(\text{endpos}/\text{right}\) 等價類)
首先,對於串 \(S\) 中的每個子串 \(s_i\),我們將其出現的地方全部找出來,然后將這些出現的地方的尾端放到同一個類里面,然后給這個類取一個名字,叫終止節點集合,或者 \(\text{endpos}\) 集合(在某些文章中稱為 \(\text{right}\) 集合),舉個栗子,有這樣的串:
我們有一個子串 \(s_i=\tt aba\),發現有:
那么,\(\text{endpos}_i\) 集合就是 \(\{3,6\}\).
繼續,現在我們每個不同的子串都有一個 \(\text{endpos}\) 集合(顯然相同的子串沒啥用),對於這些不同的子串,我們將 \(\text{endpos}\) 集合相同的子串再放到一個集合中,叫做 \(\text{endpos}\) 等價類,將這個等價類用 \(\text{endpos}\) 集合中的元素進行代替.
那么,在這個例子中,有這些等價類(無特定順序):
可以看到,有一些 \(\text{endpos}\) 集合相同的串被歸為了同一個類.
下文中,我們將用 類 代替 \(\text{endpos}\) 等價類(能少一點就少一點).
二、自動雞
有一個叫做 \(AC\) 自動雞的東西 但是不是拿來自動過題的,它也叫 “自動雞”,但是到底什么是 "自動雞" 呢?
我們認為一個東西是 "自動雞",有一些基本概念:
- 這里的自動雞其實是有限狀態自動雞(\(\text{FSM}\));
- 自動雞不是 算法/數據結構,只是一種數學模型,對於同一個問題可能有很多只雞,但是不是所有的雞都滿足我們解決問題時的資源消耗(比如時間和空間);
同時,一只雞由這幾個部分組成(雖然下文不會用到):
- 字符集 \(\Sigma\),表示這只雞只能輸入這些字符;
- 狀態集合 \(Q\),即在自動雞建好之后形成的的 \(DAG\) 上的頂點;
- 起始狀態 \(start/s\),不解釋;
- 接受狀態集合 \(F\),有 \(F\subseteq Q\);
- 轉移函數 \(\delta\),即點之間的轉移方法;
明白概念即可,下文大概率用不到.
貳、終止節點集合與類的億些特性及證明
對於上文的類有一些特性 並給它們取了名字,接下來給出並證明:
一、同類即后綴
如果有兩個串 \(S,T(|S|\le |T|)\)(其實取不到等號) 同屬一個類,那么一定有 \(S\) 是 \(T\) 的后綴.
證明:\(\mathcal Obviously\).
二、從屬或無關
\(\forall S,T(|S|\le |T|)\),他們的 \(\text{endpos}\) 集合只有兩個情況:
證明:\(\mathcal Obviously\).
三、同類長連續
屬於同一個類中的所有串,他們的長度一定是連續的,同時,如果按照長度從小到大排序,那么一定有前一個串是后一個串去掉第一個字符的后綴,或者說,后一個串是前一個串在前面加上了一個字符.
證明:\(\mathcal Obviously\).
所以,如果我們想要儲存一個類,只需要存在最短串長度與最長串的長相就可以了.
四、類數為線性
類的個數為 \(\mathcal O(n)\) 級別的.
證明:對於兩個類 \(\mathcal{X,Y}\),由 "從屬或無關" 我們可以知道 \(\mathcal{X,Y}\) 要么包含要么不相交,同時,我們也可以找到同一個集合 \(\mathcal Z\) 滿足 \(\mathcal{X,Y}\subseteq \mathcal Z\),那么,我們可以得到 \(\mathcal{X,Y}\) 其實是 \(\mathcal Z\) 進行了分裂得到的兩個類,其實,\(\mathcal Z\) 可以一次性分裂成兩個或更多個類,但由於我們最開始的集合 \(\mathcal Z=\{1,2,3,...n\}\),即 \(|\mathcal Z|=n\),由線段樹對於集合最多的不交划分結論可知,總類的數量不會超過 \(\mathcal O(2n)\),實際上最多 \(2n-1\) 個類.
終於不是顯然了.
五,鄰類鄰長度
對於一個類 \(\mathcal X\),如果有一個 \(\mathcal Y\) 滿足 \(\mathcal X\subseteq \mathcal Y\),且不存在 \(\mathcal Z\) 滿足 \(\mathcal X\subseteq \mathcal Z\subseteq Y\),即 \(\mathcal X\) 與 \(\mathcal Y\) 是直接父子關系,那么有 \(\mathcal X\) 中的最短的串 \(S\) 與 \(\mathcal Y\) 中最長的串 \(T\) 滿足 \(|S|=|T|+1\).
證明:對於一個集合的分裂,其實就是在最長的串 \(A\) 前面加上一個字符得到新的字符串 \(B\),那么,\(B\) 的出現的限制條件一定比 \(A\) 更強,即 \(B\) 所屬的類就變成了 \(A\) 所屬的類 \(\mathcal P\) 的子集了,為甚說 \(A,B\) 一定不在同一個類中呢?如果在同一類中,那么 \(A\) 就不是最長的串而是 \(B\) 了,而得到的 \(B\) 就是 \(\mathcal P\) 的某個子集中最短的一個了,那么就有 \(|A|+1=|B|\).
所以,我們只要知道詳細父子關系,那么如果我們想要表示一個類只需要存下最長的串的樣子就可以了.
叄、如何建自動雞
其實在證明中已經使用了一些類之間存在父子關系的特性了.
定義直接父子關系:對於一個類 \(\mathcal X\),如果有一個 \(\mathcal Y\) 滿足 \(\mathcal X\subseteq \mathcal Y\),且不存在 \(\mathcal Z\) 滿足 \(\mathcal X\subseteq \mathcal Z\subseteq Y\),那么 \(\mathcal X\) 與 \(\mathcal Y\) 是直接父子關系.
我們按照類的直接父子關系,建出一棵樹,這棵樹叫做 \(\text{parent tree}\),這個 \(\text{parent tree}\) 就是 \(SAM\) 的骨架,但是自動雞只有這棵樹似乎什么用都沒有,就像 \(\tt fail\) 指針基於 \(\tt trie\) 樹得到 \(AC\) 自動雞一樣,我們得在 \(\text{parent tree}\) 上加些東西.
首先,根據一些證明的說明,沿 \(\text{parent tree}\) 邊就是在串的前面動手腳,但是我們還需要在串的后面做手腳,這個時候就需要加入新的邊了,表示在串的后面加些字符,這就是自動雞上面的邊.
給出代碼,做出進一步說明:
/** @brief the size should be two times of the origin*/
int tre[maxn * 2 + 5][30];
int lenth[maxn * 2 + 5];
int fa[maxn * 2 + 5];
/** @brief the number of nodes*/
int ncnt = 1;
/** @brief this variable is necessary, it records the id of the node of the origin string*/
int lst = 1;
/**
* pay attention : node 1 is an empty node, it represent root
* and the length of this node is 0
*/
inline void add(const int c){
// the id of the original string
int p = lst;
// the id of new node or class
int u = lst = ++ ncnt;
// just means the length increases by 1
lenth[u] = lenth[p] + 1; sz[u] = 1;
/** @brief
* if the suffix of the original string
* add a new character @p c hasn't appeared yet,
* then we connect the represent node with the new node,
* it means that we can get a new class if we add @p c to the end
*/
for(; p && !tre[p][c]; p = fa[p]) tre[p][c] = u;
/** @brief
* if all suffixes haven't appeared before after adding @p c
* we'll reach the situation where @p p equals to 0,
* which means the father of the new class is 1(root)
*/
if(!p) fa[u] = 1;
/** @brief if the situation is not like this
* just means some suffixes have appeared before after adding @p c
* which means the new class is a split version of a certain class(actually class @p tre[p][c] )
*/
else{
/** @brief
* of course @p q is the original version of the new class,
* that is, @p q is apparently the father of @p u,
* but there are some other questions that
* some suffixes of @p q will appear at n,
* in other words, some part of @p q will add END POINT @p n (present length),
* which will make a different to class @p q and
* cause the split of class @p q.
* We're going to judge whether @p q should be split or not,
* if the answer is true, we're supposed to handle the split
*/
int q = tre[p][c];
/** @brief
* if all suffix have appeared before after adding @p c ,
* then there's no need to split class @p q ,
* because add END POINT will add n(present length)
* we just need to connect the new class @p u with it father
*/
if(lenth[q] == lenth[p] + 1) fa[u] = q;
/** @brief
* if not, then we're going to find out
* which part of class @p q is going to add END POINT @p n (present length),
* and we split it from here
*/
else{
/** @brief part to add new END POINT
* now node @p q represent a class which won't add new END POINT
* obviously that class @p q and class @p u are the subset of class @p split_part
*/
// pay attention : the new node has no real meaning, so the size shouldn't be added.
int split_part = ++ ncnt;
rep(i, 0, 25) tre[split_part][i] = tre[q][i];
fa[split_part] = fa[q];
lenth[split_part] = lenth[p] + 1;
fa[q] = fa[u] = split_part;
/** @brief
* because the original node @p q is split to two part,
* @p split_part and @p q (another meaning)
* then if an edge which connect a certain node with origin @p q ,
* it is supposed to be changed
*/
for(; p && tre[p][c] == q; p = fa[p])
tre[p][c] = split_part;
}
}
}
首先看變量定義:
/** @brief the size should be two times of the origin*/
int tre[maxn * 2 + 5][30];
int lenth[maxn * 2 + 5];
int fa[maxn * 2 + 5];
\(\tt tre[][]\) 表示的即為自動雞上面的邊;
\(\tt fa[]\) 表示自動雞上的父子關系,即我們通過保存父親節點來維護整個 \(\text{parent tree}\);
\(\tt lenth[]\) 表示的每個點代表的類中,最長的串有多長;
接下來進入 \(\tt add()\) 函數:
inline void add(const int c){
這個參數 \(\tt c\) 表示我們要在串的末尾插入的字符;
// the id of the original string
int p = lst;
// the id of new node or class
int u = lst = ++ ncnt;
// just means the length increases by 1
lenth[u] = lenth[p] + 1;
\(\tt p\) 表示我們在插入 \(\tt c\) 之前,代表整個串的點的編號是多少;
\(\tt u\) 代表我們要將插入 \(\tt c\) 之后,代表新的整個的串的點的編號;
\(\tt lenth[u]\) 顯然就是在未插入之前的長度基礎上增加一;
/** @brief
* if the suffix of the original string
* add a new character @p c hasn't appeared yet,
* then we connect the represent node with the new node,
* it means that we can get a new class if we add @p c to the end
*/
for(; p && !tre[p][c]; p = fa[p]) tre[p][c] = u;
接下來,我們考慮對於原串 \(S\),在增加 \(\tt c\) 之后,原來的 \(|S|-1\) 個后綴全都有了新的樣子(在原來的基礎上在末尾加上 \(\tt c\)),同時,還多出來一個新的后綴——單獨一個 \(\tt c\),我們稱這些東西為新后綴,這個循環要做的事情就是,這些新后綴如果在之前沒有出現過,那么我們要給他們歸為一個新的類——\(\text{endpos}=\{|S|+1\}\),同時,顯然,后綴越長,它越不容易出現,所以我們從代表 \(S\) 的點 \(\tt p\) 開始向上走,表示從長到短枚舉原來的后綴,對於沒出現過的,即 \(\tt tre[p][c]=0\),我們將他們連接到新的類——代表 \(\text{endpos}=\{|S|+1\}\) 的點 \(\tt u\) 上,表示在這些后綴后加上 \(\tt c\) 可以到達新的類;
if(!p) fa[u] = 1;
/** @brief if the situation is not like this
* just means some suffixes have appeared before after adding @p c
* which means the new class is a split version of a certain class(actually class @p tre[p][c] )
*/
如果上一個循壞直接走到空點,也就是說對於原串 \(S\) 的所有后綴,在加上 \(\tt c\) 之后,在之前的串中都沒見過,那么它的父節點就是 \(\text{endpos=}[1,n]\cap \mathcal Z\) 的根節點了.
else{
/** @brief
* of course @p q is the original version of the new class,
* that is, @p q is apparently the father of @p u,
* but there are some other questions that
* some suffixes of @p q will appear at n,
* in other words, some part of @p q will add END POINT @p n (present length),
* which will make a difference to class @p q and
* cause the split of class @p q.
* We're going to judge whether @p q should be split or not,
* if the answer is true, we're supposed to handle the split
*/
int q = tre[p][c];
否則,則說明有些新后綴在之前出現過,即有些后綴的 \(\text{endpos}\) 集合不只是 \(\{|S|+1\}\),還有些其他的東西,同時,由於之前全部的后綴的點的類中都加上 \(|S|+1\) 這個元素,那么此時,僅僅只代表 \(|S|+1\) 這個元素的類的點 \(\tt u\) 肯定就是其他點的兒子了,那 \(\tt u\) 的父親究竟是誰?顯然就是之前循環中第一個不滿足條件的 \(\tt p\) 的 \(\tt tre[p][c]\),我們稱這個點為 \(\tt q\).
同時,對於 \(\tt q\),由於它和 \(\tt p\) 並非有實際父子關系,而是由自動雞連接的 你居然不是我親生的,也就是說 \(\tt q\) 代表的串並非全是原串 \(S\) 的后綴(但一定會有一個 \(S\) 的后綴,即在 \(\tt p\) 所代表的后綴之后加上一個 \(\tt c\) 的后綴 ),但是我們找到它的原因是什么——它其中的某些串在 \(S\) 加上 \(\tt c\) 之后,會在 \(|S|+1\) 再出現一次,也就是有些串的 \(\text{endpos}\) 就會改動,同樣的,\(\tt q\) 中有些串又不會改動,這下出了個問題——\(\tt q\) 所代表的類中的串的 \(\text{endpos}\) 集合不一樣了,根據定義已經不能再將他們歸為同一類,所以接下來我們要考慮將 \(\tt q\) 分裂——加上 \(|S|+1\) 的部分,和沒有加上 \(|S|+1\) 的部分;
/** @brief
* if all suffix have appeared before after adding @p c ,
* then there's no need to split class @p q ,
* because add END POINT will add n(present length)
* we just need to connect the new class @p u with it father
*/
if(lenth[q] == lenth[p] + 1) fa[u] = q;
上文提及,\(\tt q\) 中一定會包含一個 \(S\) 的后綴,即在 \(\tt p\) 所代表的后綴之后加上一個 \(\tt c\) 的后綴,但是,如果我們發現 \(\tt q\) 類中最長的就是這個后綴(下文稱其為特征串),由 “同類即后綴” 意味着 \(\tt q\) 類中所有串的 \(\text{endpos}\) 集合都同時加入 \(|S|+1\) 這個元素,那么這個 \(\tt q\) 類就沒有分裂的必要了;
/** @brief
* if not, then we're going to find out
* which part of class @p q is going to add END POINT @p n (present length),
* and we split it from here
*/
else{
/** @brief part to add new END POINT
* now node @p q represent a class which won't add new END POINT
* obviously that class @p q and class @p u are the subset of class @p split_part
*/
int split_part = ++ ncnt;
否則,則意味着 \(\tt q\) 逃不掉分裂的命運,接下來我們要考慮的是從哪里裂開,先申請一個新的點 \(\tt split\_part\) 當作分裂出去的部分(此處認定分裂部分為加上 \(|S|+1\) 元素的部分)的編號.
rep(i, 0, 25) tre[split_part][i] = tre[q][i];
fa[split_part] = fa[q];
lenth[split_part] = lenth[p] + 1;
將 \(\tt p\) 的信息繼承到 \(\tt split\_part\) 上,同時,顯然我們分裂的點就是特征串(由 “同類即后綴” 同理可得),顯然,原 \(\tt q\)(沒加上元素 \(|S|+1\))就是多出 \(|S|+1\) 點的 \(\tt split\_part\) 類的一個子集,那么可以確定他們有直接父子關系了 (僅僅只多出一個元素,顯然不可能找到 \(\mathcal Z\) 滿足 \(\tt q\subseteq\mathcal Z\subseteq \tt \tt split\_part\)),那么我們將 \(\tt q\) 的父親設置為 \(\tt split\_part\),同樣,只有一個元素 \(|S|+1\) 的 \(\tt u\) 類的父親肯定也是 \(\tt split\_part\) 類了.
/** @brief
* because the original node @p q is split to two part,
* @p split_part and @p q (another meaning)
* then if an edge which connect a certain node with origin @p q ,
* it is supposed to be changed
*/
for(; p && tre[p][c] == q; p = fa[p])
tre[p][c] = split_part;
但是,我們確實是將 \(\tt split\_part\) 分裂出去了,但是,這個 \(\tt split\_part\) 是代表原來的 \(\tt q\) 的,但是還有一些點是沿着自動雞連接在舊的 \(\tt q\) 上,我們要將他們改過來,將他們和 \(\tt split\_part\) 連接.
然后,插入新點結束,構建自動雞時只需:
rep(i, 1, n) add(s[i] - 'a');
貼一發整合代碼:
const int maxn = 1e6;
int tre[maxn * 2 + 5][30];
int lenth[maxn * 2 + 5];
int fa[maxn * 2 + 5];
int sz[maxn * 2 + 5];
int ncnt = 1;
int lst = 1;
char s[maxn + 5];
int n; // the length of string s
inline void input(){
scanf("%s", s + 1);
n = strlen(s + 1);
}
inline void add(const int c){
int p = lst;
int u = lst = ++ ncnt;
lenth[u] = lenth[p] + 1;
for(; p && !tre[p][c]; p = fa[p]) tre[p][c] = u;
if(!p) fa[u] = 1;
else{
int q = tre[p][c];
if(lenth[q] == lenth[p] + 1) fa[u] = q;
else{
int split_part = ++ ncnt;
rep(i, 0, 25) tre[split_part][i] = tre[q][i];
fa[split_part] = fa[q];
lenth[split_part] = lenth[p] + 1;
fa[q] = fa[u] = split_part;
for(; p && tre[p][c] == q; p = fa[p])
tre[p][c] = split_part;
}
}
}
signed main(){
input();
rep(i, 1, n) add(s[i] - 'a');
}
注意:如果需要統計某個點的子節點大小,在將 \(\tt q\) 裂開之后產生的新點不能計算 \(\tt sz\),因為它只是一個裂開的點為了表示一個新類,並不會對總點數產生貢獻.
肆、復雜度分析
一、點數線性
點數即類數,上文已證明,最多 \(2n-1\) 個 類/點.
二、邊數線性
考慮自動雞有以下性質:
- 從 \(s\) 開始,沿不同的路徑,最終一定會走到不同的子串;
- 由 “同類即后綴”,以及 “同類長連續”,可以說明,只要這個類中有一個串是整個串的后綴,那么整個類都是串的后綴(其實從另一個角度說,同一個類中的串,區別只在於他們在前面加字符,但是在后面的字符是不動的)
然后,我們進行一個定義:定義廣義環為忽略邊的方向,將所有邊視為無向邊之后形成的環成為廣義環.
現在,我們可以開始證明邊數不超過 \(3n-4\) 了.
首先,假設我們有了一個自動雞,但是他們還沒有連邊,我們從 \(s\) 開始走后綴,現在規定走后綴的規則:
- 每次走后綴只有一次花費;
- 如果走的邊和原來的邊沒有形成廣義環,那么我們走這條邊並將這條邊添加進自動雞,並且這條邊不會使用花費;
- 如果走的邊和原來的邊形成了廣義環,那么我們走這條邊並將這條邊添加進自動雞,並且使用花費;
如果我們沿這些規律走后綴,最后最多只會有 \(3n-4\) 條邊,
三、\(\tt add()\) 函數復雜度證明
我再貼一個代碼:
inline void add(const int c){
int p = lst;
int u = lst = ++ ncnt;
lenth[u] = lenth[p] + 1;
for(; p && !tre[p][c]; p = fa[p]) tre[p][c] = u;
if(!p) fa[u] = 1;
else{
int q = tre[p][c];
if(lenth[q] == lenth[p] + 1) fa[u] = q;
else{
int split_part = ++ ncnt;
rep(i, 0, 25) tre[split_part][i] = tre[q][i];
fa[split_part] = fa[q];
lenth[split_part] = lenth[p] + 1;
fa[q] = fa[u] = split_part;
for(; p && tre[p][c] == q; p = fa[p])
tre[p][c] = split_part;
}
}
}
其實這里就幾個循環,而對於前兩個,我們都是在加邊,但是由於我們邊數已經是 \(3n-4\) 的上界了,那么這倆循環加起來總共也不超過 \(3n-4\),但是我們考慮我們的第三個循環:
for(; p && tre[p][c] == q; p = fa[p])
tre[p][c] = split_part;
這個不好分析,我們考慮整個 \(\tt add()\) 在干什么:
- 從 \(lst\) 向上爬,這個過程會減小可能成為 \(u\) 的 \(\tt fa[u]\) 的 \(\text{minlen}\)(就是類里面最小的字符串長度);
- 爬到頭了,從 \(p\) 走到 \(q\),這個過程會讓可能成為 \(u\) 的 \(\tt fa[u]\) 的 \(\text{minlen}\) 不多不少 \(+1\);
- 給 \(u\) 認父親,然后讓 \(u\) 變成 \(lst\)(也就是下一次的 \(u\));
我們發現,每次 \(fa[u]\) 的 \(\text{minlen}\) 最多只會增加一,但是減少卻可能一次性減少很多,這類似於 \(\tt SA\) 求 \(\text{heit}\) 數組的過程,那么總復雜度大概在 \(\mathcal O(n)\) 級別.
實際上,如果我們將 \(\text{minlen}(fa[u])\) 當成勢能函數,可能會更好理解.
伍、應用
部分引自 \(\tt oi-wiki\),對於部分 我知道的 可以使用 \(SA\) 解決的也會大致描述,同樣,如果有其它如使用 \(\tt kmp\) 的巧妙方法亦會大致描述.
一、檢查子串
- 使用 \(SAM\):
檢查 \(T\) 是否是 \(S\) 的某個子串.
后綴自動雞中實際上保存了 \(S\) 的所有子串信息,所以你只需要從開始節點沿自動雞邊走就可以了.
- 使用 \(SA\):
將 \(S\) 和 \(T\) 拼在一起,中間用分隔符隔開,判斷 \(\text{heit}\) 即可.
二、本質不同串個數
- 使用 \(SAM\):
\(SAM\) 沿自動雞邊走形成的串本來就是本質不同的子串,所以,我們只需要統計從開始節點走的不同路徑數,由於是 \(DAG\),可以直接跑拓撲 \(DP\).
- 使用 \(SA\):
每個后綴長度減去 \(\text{heit}\) 之和.
三、第 k 大子串
- 使用 \(SAM\):
如果是去重,那么每個點的 \(sz\) 定為 \(1\),即表示每個字串都只出現一次而不因 \(\text{parent tree}\) 樹的出現而改變,反之,\(sz\) 即為其 \(\text{parent tree}\) 子樹的大小.
然后,我們處理出一個 \(\tt sum\) 數組表示經過這個點的子串有多少,那么就有
處理出來之后用類似 \(\tt trie\) 樹的做法即可.
- 使用 \(SA\):
如果是去重,處理出 \(\tt SA\) 之后,由每個后綴都有 \(n-sa[i]+1-heit[i]\) 個本質不同的子串,然后就可以解決.
如果是非去重,這里直接引用 大佬 的解決方案.
因為我們前面的字母是確定的,那么當前面的字母一樣的時候,后面一個字母一定是單調不下降的。
那我們就能二分求出這個字母最后一個的位置。
我們建一個后綴長度的前綴和,那我們就能求出以這個字母開頭的子串有多少個。
當個數大於 \(k\) 時,就確定了這個字母,否則 \(k\) 減去,繼續枚舉下一個字母。
枚舉完一位繼續下一位,那我們就能把范圍縮小,知道求出答案。
實際上本質和 \(SAM\) 的做法差不多.
四、二元最長公共子串(LCS)
- 使用 \(SAM\):
對於一個串建立 \(SAM\),拿另一個串在 \(SAM\) 上能跑多遠跑多遠,跑不了了就往父親跳.
- 使用 \(SA\):
兩個串用 \(SA\) 經典流氓做法接在一起(注意中間用分隔符隔開),處理出 \(\tt sa\) 之后將 \(\tt heit\) 排序從大到小枚舉長度,直到一個屬於兩個串的 \(\tt heit\) 滿足標准后就找到長度(如果要找長什么樣子也可以,這里不做贅述).
五、最小循環位移
- 使用 \(\tt kmp\):
先找到 \(n\) 位置對應的 \(nxt\),那么最小循環位移就可能是 \(n-nxt[n]\),檢查一下,如果不滿足就 \(GG\).
- 使用 \(SAM\):
復制一個 \(S\) 並對其建立 \(SAM\),然后在 \(SAM\) 上尋找最小的長度為 \(|S|\) 的路徑即可.找最小的,我們只需要貪心往最小字符走即可.
- 使用 \(SA\):
理論上可以當 \(\tt kmp\) 用,我們對於反串排出 \(\tt sa\),然后看 \(S'[1...n]\) 的位置,那么長度要么就是它和它前一名、要么就是它和它后一名的 \(LCP\),然后我們暴力檢驗就可以了.
六、子串出現次數
- 使用 \(SAM\):
找到子串對應的點,然后算出這個點的 \(sz\) 就可以了.
- 使用 \(SA\):
用流氓做法,把子串接在后面然后處理出 \(\tt sa\),然后看屬於兩個串的 \(heit\) 剛剛好是子串長度的 \(heit\) 個數即可.
七、多元最長公共子串(exLCS)
- 使用 \(SAM\):
兩種思路:
- 對於第一個建立 \(SAM\),然后其他的在這個上跑,對於每個點,記錄 \(f[i]\) 表示走到 \(i\) 能夠匹配上的最長公共子串的長度,注意 \(f[i]\le \text{longest}(i)\),最后跑拓撲,用 \(f[i]\) 更新 \(f[fa[i]]\),得到匹配完某個串之后的 \(f[]\),然后記錄在 \(ans[i]\) 中,其中 \(ans[i]\) 表示第 \(i\) 個點的歷史中能匹配的最小長度(因為我們要求的是滿足所有的串),最后在 \(ans[i]\) 中找最大就可以了.
- 設 \(T=S_1+D_1+S_2+D_2+...+D_{n-1}+S_n+D_n\),其中 \(S_i\) 是我們想要找的,\(D_i\) 是對應的分隔符,我們對於 \(T\) 建立 \(SAM\),然后,如果 \(S_i\) 中有一個子串,那么存在一條路徑從 \(D_i\) 不經過其他的 \(D_j\) 的路徑. 然后我們處理連通性,使用 \(\tt BFS\) 等搜索算法之族以及動規計算,然后,答案就是所有的 \(D_i\) 都能到達的點中 \(\text{longest}\) 的最大值.
- 使用 \(SA\):
流氓將所有串暴力接起來,從大到小枚舉 \(heit\) 長度,並用並查集枚舉每一個合並的塊中有多少不同的集合,當某個連通塊中存在了所有的串,此時枚舉的長度就是答案串的長度,尋找就隨意了.