「Note」字符串方向 - 自动机相关
1. AC 自動(dòng)機(jī) ACAM
1.1. 簡(jiǎn)介
AC 自動(dòng)機(jī)用于解決多模式串匹配問(wèn)題,例如求多個(gè)模式串在文本串中的出現(xiàn)次數(shù)。顯著地,它的應(yīng)用實(shí)際上非常廣泛。
借助 KMP 的思想,我們對(duì) Trie 樹(shù)上的每個(gè)節(jié)點(diǎn)構(gòu)造其失配指針 \(fail_i\),指向?qū)τ诋?dāng)前字符串的最長(zhǎng)后綴(其他(前綴)作為當(dāng)前串后綴的最長(zhǎng)的一個(gè)),顯著地,每個(gè)節(jié)點(diǎn)的失配指針都指向一個(gè)更短的狀態(tài)。當(dāng)這樣的后綴不存在時(shí),失配指針會(huì)指向表示空串的根節(jié)點(diǎn)。
考慮如何構(gòu)建 \(fail_i\):
根據(jù)每個(gè)節(jié)點(diǎn)的失配指針都指向一個(gè)更短的狀態(tài)這個(gè)性質(zhì),考慮用 BFS 解決 \(fail_i\) 的構(gòu)建,對(duì)于當(dāng)前節(jié)點(diǎn) \(now\) 來(lái)說(shuō),假設(shè)深度較小的節(jié)點(diǎn)都已經(jīng)被處理完了。
現(xiàn)在假設(shè)當(dāng)前節(jié)點(diǎn) \(i\) 由 \(fa_i\) 經(jīng)過(guò)字符 \(ch\) 轉(zhuǎn)移過(guò)來(lái),使 \(fail_i\leftarrow trans(fail_{fa_i},ch)\),若不存在 \(fail_{fa_i}\) 通過(guò) \(ch\) 轉(zhuǎn)移到的某一節(jié)點(diǎn),則嘗試使 \(fail_i\leftarrow trans(fail_{fail_{fa_i}},ch)\)。直到 \(fail\) 指向根節(jié)點(diǎn),說(shuō)明根本不存在合法前綴,我們使 \(fail_i\leftarrow rt\)。
特殊地,若不存在 \(trans(fa,ch)\) 這個(gè)轉(zhuǎn)移方式,則直接令 \(trans(fa,ch)\leftarrow trans(fail_{fa_i},ch)\)。
1.2. 常見(jiàn)技巧
1.2.1 fail 樹(shù)的性質(zhì)
構(gòu)建的 \(fail\) 指針會(huì)形成一棵樹(shù),稱為 fail 樹(shù)。這不是廢話嗎。
- fail 樹(shù)為一顆有根樹(shù),可以進(jìn)行樹(shù)剖等樹(shù)上操作。
- 對(duì)于節(jié)點(diǎn) \(p\) 與其對(duì)應(yīng)字符串 \(t_p\),對(duì)于任意一個(gè)子樹(shù)內(nèi)節(jié)點(diǎn) \(q\),都有 \(t_p\) 是 \(t_q\) 的后綴。逆命題亦成立。
- 設(shè) \(cnt_p\) 表示作為 \(t_p\) 后綴的字符串?dāng)?shù)量。若無(wú)重復(fù)串,則 \(cnt_p\) 為樹(shù)上節(jié)點(diǎn) \(p\) 到根節(jié)點(diǎn)上字符串節(jié)點(diǎn)數(shù)量。
1.2.2 應(yīng)用
ACAM 可以與 DP 結(jié)合,在自動(dòng)機(jī)中進(jìn)行 DP。
1.3. 例題
\(\color{blueviolet}{P5357}\)
時(shí)間瓶頸在于每次跳 \(tail\) 的重復(fù)訪問(wèn),考慮經(jīng)過(guò)每個(gè)點(diǎn)時(shí)記錄權(quán)值,最后一起統(tǒng)計(jì),可以采用 DFS 或者拓?fù)渑判颉?/p>
$\text{Code}$:
#include<bits/stdc++.h>
#define LL long long
#define UN unsigned
using namespace std;
//--------------------//
//IO
inline int rd()
{
int ret=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
while(ch>='0'&&ch<='9')ret=ret*10+ch-'0',ch=getchar();
return ret*f;
}
//--------------------//
const int N=2e6+5,Ch=30;
int n;
char str[N];
int id[N],ans[N];
struct Edge
{
int to,nex;
}edge[N];
int etot,head[N];
void add(int from,int to)
{
edge[++etot]={to,head[from]};
head[from]=etot;
return;
}
struct ACAM
{
struct Trie_Node
{
int nex[Ch];
int fail,flag,cnt;
}t[N];
int tot,fcnt;
void insert(char *s,int temp)
{
int now=0,len=strlen(s+1);
for(int i=1;i<=len;i++)
{
if(!t[now].nex[s[i]-'a'+1])
t[now].nex[s[i]-'a'+1]=++tot;
now=t[now].nex[s[i]-'a'+1];
}
if(!t[now].flag)
t[now].flag=++fcnt;
id[temp]=t[now].flag;
return;
}
void get_fail()
{
queue<int>q;
for(int i=1;i<=26;i++)
{
if(t[0].nex[i])
q.push(t[0].nex[i]);
}
while(!q.empty())
{
int now=q.front();
q.pop();
for(int to,i=1;i<=26;i++)
{
to=t[now].nex[i];
if(!to)
{
t[now].nex[i]=t[t[now].fail].nex[i];
continue;
}
t[to].fail=t[t[now].fail].nex[i];
q.push(to);
}
}
return;
}
void get_ans(char *s)
{
int len=strlen(s+1),now=0;
for(int i=1;i<=len;i++)
{
now=t[now].nex[s[i]-'a'+1];
t[now].cnt++;
}
return;
}
void build()
{
for(int i=1;i<=tot;i++)
add(t[i].fail,i);
return;
}
void DFS(int now)
{
//printf("%d %d\n",now,t[now].cnt);
for(int to,i=head[now];i;i=edge[i].nex)
{
to=edge[i].to;
DFS(to);
t[now].cnt+=t[to].cnt;
}
if(t[now].flag)
ans[t[now].flag]=t[now].cnt;
return;
}
}A;
//--------------------//
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%s",str+1),A.insert(str,i);
A.get_fail();
scanf("%s",str+1);
A.get_ans(str);
A.build();
A.DFS(0);
for(int i=1;i<=n;i++)
printf("%d\n",ans[id[i]]);
return 0;
}
2. 后綴自動(dòng)機(jī) SAM
2.1. 簡(jiǎn)介
2.1.1. 基本定義與結(jié)論
SAM 一般用于在線性時(shí)間內(nèi)解決如下問(wèn)題:
- 在一個(gè)字符串中求另一字符串出現(xiàn)的位置。
- 一個(gè)字符串的本質(zhì)不同的子串個(gè)數(shù)。
SAM 的定義:一個(gè)長(zhǎng)為 \(n\) 的字符串 \(s\) 的SAM 是一個(gè)接受 \(s\) 的所有后綴的最小的有限狀態(tài)自動(dòng)機(jī)。
較為人話的說(shuō)法:
- SAM 是一張有向無(wú)環(huán)圖。每個(gè)節(jié)點(diǎn)為一個(gè)狀態(tài),邊則為狀態(tài)間的轉(zhuǎn)移。
- 存在一個(gè)源點(diǎn) \(t_0\),稱作初始狀態(tài),其他狀態(tài)均可從 \(t_0\) 出發(fā)到達(dá)。
- 每個(gè)轉(zhuǎn)移(邊)對(duì)應(yīng)一個(gè)字符(一條路徑表示一個(gè)字符串),從一個(gè)狀態(tài)(節(jié)點(diǎn))出發(fā)的轉(zhuǎn)移均不同。
- 從初始狀態(tài) \(t_0\) 出發(fā),最終轉(zhuǎn)移到一個(gè)終止?fàn)顟B(tài),則此徑代表的字符串一定是原字符串 \(s\) 的一個(gè)后綴,\(s\) 的每個(gè)后綴都可以用一條這種路徑表示。
- 滿足上述條件的自動(dòng)機(jī)中,SAM 的節(jié)點(diǎn)數(shù)量最少。
SAM 的較為重要的一條性質(zhì):
- 從初始狀態(tài)出發(fā)到任意狀態(tài)的路徑與串 \(s\) 的所有子串(本質(zhì)不同)一一對(duì)應(yīng)。
接下來(lái)給出一些定義和符號(hào)表示:
- \(c_{p,q}\):轉(zhuǎn)移 \(p\to q\) 代表的字符。
- \(\mathrm{st}(p,c)\):狀態(tài) \(p\) 經(jīng)過(guò)字符 \(c\) 轉(zhuǎn)移所到達(dá)的狀態(tài)。
- \(\mathrm{endpos}(t)\):字符串 \(t\) 在原字符串中所有結(jié)束位置的集合。
- 等價(jià)類:對(duì)于 \(\mathrm{endpos}\) 集合相同的子串,我們將它們劃分為一個(gè)等價(jià)類,作為一個(gè)狀態(tài)。
- \(\mathrm{ep}(p)\):狀態(tài) \(p\) 所對(duì)應(yīng)的 \(\mathrm{endpos}\) 集合。
- \(\mathrm{substr}(p)\):狀態(tài) \(p\) 所表示的所有子串的集合。
- \(\mathrm{longest}(p)\):狀態(tài) \(p\) 所表示的所有子串中,長(zhǎng)度最長(zhǎng)的那一個(gè)子串。
- \(\mathrm{shortest}(p)\):狀態(tài) \(p\) 所表示的所有子串中,長(zhǎng)度最短的那一個(gè)子串。
- \(\mathrm{len}(p)\):狀態(tài) \(p\) 所表示的所有子串中,長(zhǎng)度最長(zhǎng)的那一個(gè)子串的長(zhǎng)度。
- \(\mathrm{minlen}(p)\):狀態(tài) \(p\) 所表示的所有子串中,長(zhǎng)度最短的那一個(gè)子串的長(zhǎng)度。
方便理解,我們?cè)俅握砩鲜龆x。SAM 的每個(gè)狀態(tài)對(duì)應(yīng)一個(gè)等價(jià)類,即 \(\mathrm{endpos}\) 集合相同的子串所組成的狀態(tài)。具體地,我們給出例子。假設(shè)現(xiàn)有串 \(s=\texttt{"abcab"}\),則 \(\mathrm{endpos}(\texttt{"ab"})=\mathrm{endpos}(\texttt{"b"})={2,5}\),而串 \(\texttt{"ab"},\texttt{"b"}\) 便屬于統(tǒng)一等價(jià)類。
\(\mathrm{longest}(p),\mathrm{shortest}(p),\mathrm{len}(p),\mathrm{minlen}(p)\) 則描述了狀態(tài) \(p\) 對(duì)應(yīng)的字符串集合 \(\mathrm{substr}(p)\) 中最長(zhǎng)、最短的字符串以及它們的長(zhǎng)度。
下面介紹兩個(gè)結(jié)論:
- 對(duì)于兩個(gè)非空字符串 \(x,y\)(\(|x|\leq |y|\)),要么有 \(\mathrm{endpos}(x)\subseteq \mathrm{endpos}(y)\),要么有 \(\mathrm{endpos}(x)\cup\mathrm{endpos}(y)=\varnothing\)
- 對(duì)于一個(gè)狀態(tài) \(p\),其包含的字符串長(zhǎng)度連續(xù),且較短者是較長(zhǎng)者的后綴。
兩個(gè)結(jié)論的證明過(guò)程并不復(fù)雜,簡(jiǎn)單思考也可以感性理解,在這里不給出具體證明,具體可參考 Alex_Wei 的博客或者 OI-Wiki,鏈接見(jiàn)參考資料部分。
總結(jié)以上兩個(gè)性質(zhì)有:
- 對(duì)于一個(gè)子串 \(t\) 的所有后綴,其 \(endpos\) 集合的大小隨著后綴長(zhǎng)度減小而單調(diào)不降,并且較大的集合包含較小的集合。簡(jiǎn)單定性分析,當(dāng)后綴越短時(shí),約束條件越寬松,出現(xiàn)位置更可能多。
根據(jù)上面的性質(zhì),我們給出 SAM 中最核心的定義:
- 后綴鏈接 \(\mathrm{link}(p)\):對(duì)于所有 \(\mathrm{ep}(p)\subseteq\mathrm{ep}(q)\),\(\mathrm{link}(p)\) 指向 \(\mathrm{len}(q)\) 最大的那個(gè) \(q\)。
稍微直觀一點(diǎn)理解我們?nèi)匀挥靡粋€(gè)例子,假設(shè)現(xiàn)有串 \(s=\texttt{"babcab"}\)
我們假設(shè)狀態(tài) \(p\) 對(duì)應(yīng)著我們的字符串集合 \(\{\texttt{"cab"}\}\),對(duì)應(yīng)有 \(\mathrm{ep}(p)=\{6\}\)。
狀態(tài) \(a\) 對(duì)應(yīng)字符串集合 \(\{\texttt{"ab"}\}\),對(duì)應(yīng)有 \(\mathrm{ep}(a)=\{3,6\}\)。
狀態(tài) \(b\) 對(duì)應(yīng)字符串集合 \(\{\texttt{"b"}\}\),對(duì)應(yīng)有 \(\mathrm{ep}(b)=\{1,3,6\}\)。
根據(jù)剛才的定義,現(xiàn)有 \(\mathrm{ep}(p)\subseteq\mathrm{ep}(a),\mathrm{ep}(p)\subseteq\mathrm{ep}(b)\),且只有 \(a,b\) 兩狀態(tài)的 \(\mathrm{ep}\) 包含狀態(tài) \(p\) 的 \(\mathrm{ep}\),又有 \(\mathrm{len}(a)>\mathrm{len}(b)\),所以 \(\mathrm{link}(p)\) 應(yīng)指向狀態(tài) \(a\)。
再次重復(fù)一下 \(\mathrm{link}(p)\) 的意義,它指向了狀態(tài) \(p\) 的所有后綴狀態(tài)中(最長(zhǎng))長(zhǎng)度最大的那個(gè),易知 \(\mathrm{len}(\mathrm{link}(p))+1=\mathrm{minlen}(p)\)。
對(duì)于后綴鏈接有這樣一條性質(zhì):
- 所有后綴鏈接形成一顆以 \(t_0\) 為根的樹(shù)。(\(t_0\) 是我們最開(kāi)始定義的初始狀態(tài),它包含了空串。)
顯著的,對(duì)于任意狀態(tài)(除了 \(t_0\)),沿著后綴鏈接移動(dòng)總會(huì)達(dá)到一個(gè) \(\mathrm{len}\) 更短的狀態(tài),直到 \(t_0\)。
后綴鏈接構(gòu)成的樹(shù)本質(zhì)上是 \(\mathrm{endpos}\) 集合構(gòu)成的一棵樹(shù),我們一般稱為 Parent 樹(shù)。
2.1.2. 關(guān)鍵結(jié)論
這部分主要摘自 Alex_wei 的博客,理解構(gòu)建 SAM 的過(guò)程需要理解此部分的結(jié)論。如果你能較為直觀地理解 Parent 樹(shù),那么這部分的結(jié)論都很顯然,大部分證明請(qǐng)參考 Alex_wei 的博客。
第一組結(jié)論:
- 從任意狀態(tài) \(p\) 出發(fā)通過(guò)后綴鏈接跳轉(zhuǎn)到 \(t_0\) 的路徑,所有路徑上的狀態(tài) \(q\) 的 \([\mathrm{minlen}(q),\mathrm{len}(q)]\) 無(wú)交集,并且范圍隨著在 Parent 上的深度減小而減小,并且他們的并集形成一個(gè)連續(xù)區(qū)間 \([0,\mathrm{len}(p)]\)。
- 從任意狀態(tài) \(p\) 出發(fā)通過(guò)后綴鏈接跳轉(zhuǎn)到 \(t_0\) 的路徑,所有路徑上的狀態(tài) \(q\) 的 \(\mathrm{substr}(q)\) 的并集為 \(\mathrm{longest}(p)\) 的所有后綴。
第二組結(jié)論:
- 有任意狀態(tài) \(p\) 使得有從 \(p\) 到 \(q\) 的轉(zhuǎn)移,對(duì)于 \(\forall t_p\in \mathrm{substr}(p)\),有 \(t_p+c_{p,q}\in\mathrm{substr}(q)\)。
- 對(duì)于 \(\forall t_q\in\mathrm{substr}(q)\),存在且只存在一個(gè)狀態(tài) \(p\) 使得有從 \(p\) 到 \(q\) 的轉(zhuǎn)移,并且 \(\exist t_p\in \mathrm{substr}(p)\) 使得 \(t_p+c_{p,q}=t_q\)。
第三組結(jié)論:
- 不存在從狀態(tài) \(p\) 到狀態(tài) \(q\) 的轉(zhuǎn)移使得 \(\mathrm{len}(p)+1>\mathrm{len}(q)\)。
- 存在唯一狀態(tài) \(p\),有 \(p\) 到 \(q\) 的轉(zhuǎn)移使得 \(\mathrm{len}(p)+1=\mathrm{len}(q)\)。
- 存在唯一狀態(tài) \(p\),有 \(p\) 到 \(q\) 的轉(zhuǎn)移使得 \(\mathrm{minlen}(p)+1=\mathrm{minlen}(q)\)。
在給出第四組結(jié)論之前,我們先給出兩個(gè)定義:
- \(\mathrm{maxtrans}(q)\):有 \(p\) 到 \(q\) 的轉(zhuǎn)移使得 \(\mathrm{len}(p)+1=\mathrm{len}(q)\) 的唯一 \(p\)。
- \(\mathrm{mintrans}(q)\):有 \(p\) 到 \(q\) 的轉(zhuǎn)移使得 \(\mathrm{minlen}(p)+1=\mathrm{minlen}(q)\) 的唯一 \(p\)。
第四組結(jié)論:
- 對(duì)于轉(zhuǎn)移 \(p\to q\),一定有 \(p\) 在 Parent 樹(shù)上為 \(\mathrm{maxtrans}(q)\) 或其祖先。
- 對(duì)于轉(zhuǎn)移 \(p\to q\),一定有 \(p\) 在 Parent 樹(shù)上為 \(\mathrm{mintrans}(q)\) 或其子樹(shù)內(nèi)節(jié)點(diǎn)。
- 對(duì)于轉(zhuǎn)移 \(p\to q\),所有這樣的 \(p\) 在 Parent 樹(shù)上構(gòu)成了一條深度遞減鏈,即 \(\mathrm{mintrans}(q)\to\mathrm{maxtrans}(q)\)。
并不難理解,考慮到 Parent 樹(shù)的定義以及性質(zhì),一條從上到下的鏈中字符串長(zhǎng)度連續(xù)并且都為鏈底長(zhǎng)串的子串。
2.1.3 SAM 的構(gòu)建
至此為止,我們可以用以上的所有性質(zhì)來(lái)構(gòu)建 SAM 了。
我們考慮在前綴串 \(s[1,i-1]\) 的 SAM 基礎(chǔ)上插入當(dāng)前字符更新整個(gè) SAM。
設(shè)上一狀態(tài)(目前已插入的前綴所在狀態(tài))為 \(las\),當(dāng)前狀態(tài)為 \(cur\),狀態(tài)總數(shù)為 \(tot\)。初始時(shí) \(las,cnt\) 均為 \(1\),即我們?cè)O(shè)初始狀態(tài) \(t_0=1\)。
我們使先新建編號(hào)為 \(cur\) 賦值,\(cur\) 表示的是以當(dāng)前插入字符結(jié)尾前綴的狀態(tài),然后令 \(p\leftarrow las\),\(p\) 表示我們現(xiàn)在更新到的節(jié)點(diǎn)。
考慮轉(zhuǎn)移邊的處理,我們將 \(p\) 沿著 Parent 樹(shù)向上跳,可以保證的是每到一個(gè)節(jié)點(diǎn)都是 \(s[1,i-1]\) 的后綴,所以我們要更新其向 \(s_i\) 的轉(zhuǎn)移,若其沒(méi)有此轉(zhuǎn)移,我們就為其新建出邊,并繼續(xù)沿著 Parent 向上跳轉(zhuǎn)。直到我們到一個(gè)節(jié)點(diǎn)存在此轉(zhuǎn)移,說(shuō)明再往上的節(jié)點(diǎn)都有此轉(zhuǎn)移,就不必再更新了。
接下來(lái)考慮 Parent 樹(shù)邊的構(gòu)建,我們分三種情況討論。
情況一:
不存在一個(gè) \(p\) 有以 \(s_i\) 的轉(zhuǎn)移。
這種情況存在且只存在于 \(s_i\) 這個(gè)字符從未被加入過(guò),我們令 \(\mathrm{link}(cur)\leftarrow t_0\) 即可。
情況二:
存在 \(p\) 有以 \(s_i\) 的轉(zhuǎn)移,令 \(q=\mathrm{st}(p,s_i)\),且 \(\mathrm{len}(p)+1=\mathrm{len}(q)\)。
我們?cè)O(shè) \(las\to t_0\) Parent 樹(shù)上的路徑 \(p\) 的前一個(gè)狀態(tài)有 \(p'\),并且 \(p'\) 已經(jīng)新建了 \(s_i\) 的轉(zhuǎn)移到 \(cur\),根據(jù) Parent 性質(zhì)有 \(\mathrm{minlen}(cur)=\mathrm{len}(p')+1=(\mathrm{len}(p)+1)+1=\mathrm{len}(q)+1\),根據(jù)定義令 \(\mathrm{link}(cur)\leftarrow q\)。
情況三:
存在 \(p\) 有以 \(s_i\) 的轉(zhuǎn)移,令 \(q=\mathrm{st}(p,s_i)\),且 \(\mathrm{len}(p)+1\not=\mathrm{len}(q)\)。
當(dāng) \(\mathrm{len}(p)+1\not=\mathrm{len}(q)\),只能存在 \(\mathrm{len}(p)+1<\mathrm{len}(q)\)。狀態(tài) \(q\) 中有一部分是無(wú)法轉(zhuǎn)移到我們當(dāng)前狀態(tài) \(cur\) 的,可以理解為 \(\mathrm{substr}(q)\) 不全為 \(\mathrm{substr}(cur)\) 的后綴,因?yàn)樵?\(q\) 中存在以除 \(p\) 之外的狀態(tài)轉(zhuǎn)移過(guò)來(lái)的部分。我們考慮將 \(q\) 分為小于等于 \(\mathrm{len}(p)+1\) 和大于 \(\mathrm{len}(p)+1\) 的兩部分,并新建狀態(tài) \(cl\) 存儲(chǔ)小于等于 \(\mathrm{len}(p)+1\) 的部分。對(duì)于繼承我們需要進(jìn)行以下操作:
- \(cl\) 保存所有 \(q\) 向外的轉(zhuǎn)移。
- \(\mathrm{len}(cl)\) 應(yīng)等于 \(\mathrm{len}(p)+1\)。
- \(\mathrm{link}(cl)\) 應(yīng)等于原來(lái)的 \(\mathrm{link}(q)\)。
- 對(duì)于 Parent 樹(shù)上 \(p\to t\) 路徑上的狀態(tài),我們也應(yīng)該將原指向 \(q\) 的轉(zhuǎn)移指向 \(cl\)。
- \(\mathrm{link}(q),\mathrm{link}(cur)\) 應(yīng)等于 \(cl\)。
在構(gòu)建完 Parent 樹(shù)邊后,我們使 \(las\leftarrow cur\),退出構(gòu)建即可。
2.1.4. SAM 的時(shí)空間限制
對(duì)于 SAM 構(gòu)造、使用時(shí)間復(fù)雜度為線性的證明略復(fù)雜,仍是可參考 Alex_Wei 的博客或者 OI-Wiki。
因?yàn)槊看渭尤胱址疃嘈陆▋蓚€(gè)節(jié)點(diǎn),所以空間應(yīng)當(dāng)開(kāi)雙倍,特殊地,當(dāng)字符集很大時(shí),可以用 map 維護(hù)轉(zhuǎn)移。
2.2. 常用技巧
2.2.1. 求本質(zhì)不同子串個(gè)數(shù)
考慮每個(gè)子串存在且只存在于一個(gè)狀態(tài),考慮計(jì)數(shù)所有狀態(tài)中的子串總和,對(duì)于每個(gè)狀態(tài) \(i\) 有答案 \(\sum\mathrm{len}(i)-\mathrm{len}(\mathrm{link}(i))\)。
另一種考慮方式,SAM 上一條路徑對(duì)應(yīng)一個(gè)子串,對(duì)于每個(gè)狀態(tài)求一下以此狀態(tài)結(jié)尾的路徑數(shù)量,最后求和,可以考慮用拓?fù)渑判?。(相?dāng)于轉(zhuǎn)化為所有前綴的后綴個(gè)數(shù)。)
2.2.2. 解決匹配串問(wèn)題
較為簡(jiǎn)單的,直接在 SAM 上各種跑,失配就跳 Parent,與 KMP 的思想相似。
2.2.3. 求 \(\mathrm{endpos}\) 集合大小
每加入一個(gè)新?tīng)顟B(tài) \(cur\),為其計(jì)數(shù)器打上 \(1\),SAM 構(gòu)建好后求一下 Parent 子樹(shù)內(nèi)計(jì)數(shù)器和即可。
2.3. 例題
\(\color{blueviolet}{P3804}\)
SAM 板子。
$\text{Code}$:
#include<bits/stdc++.h>
#define LL long long
#define UN unsigned
using namespace std;
//--------------------//
//IO
inline int rd()
{
int ret=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
while(ch>='0'&&ch<='9')ret=ret*10+ch-'0',ch=getchar();
return ret*f;
}
//--------------------//
const int N=1e6+5,N2=2e6+5;
struct Edge
{
int to,nex;
}edge[N2];
int etot,head[N2];
void add(int from,int to)
{
edge[++etot]={to,head[from]};
head[from]=etot;
return;
}
char s[N];
struct SAM
{
struct SAM_Node
{
int nex[30];
int len,fa;
int cnt;
}a[N2];
int las=1,tot=1;
void insert(char ch)
{
int it=ch-'a'+1,p=las;
int cur=++tot;
a[cur].len=a[las].len+1,las=cur,a[cur].cnt=1;
while(!a[p].nex[it]&&p)
a[p].nex[it]=cur,p=a[p].fa;
if(!p)
{
a[cur].fa=1;
return;
}
int q=a[p].nex[it];
if(a[p].len+1==a[q].len)
{
a[cur].fa=q;
return;
}
int cl=++tot;
a[cl]=a[q],a[cl].cnt=0,a[cl].len=a[p].len+1;
a[cur].fa=a[q].fa=cl;
while(a[p].nex[it]==q&&p)
a[p].nex[it]=cl,p=a[p].fa;
return;
}
void build()
{
for(int i=1;i<=tot;i++)
add(a[i].fa,i);
return;
}
LL ans=0;
void DFS(int now)
{
for(int to,i=head[now];i;i=edge[i].nex)
{
to=edge[i].to;
DFS(to);
a[now].cnt+=a[to].cnt;
}
if(a[now].cnt!=1)
ans=max(ans,1LL*a[now].cnt*a[now].len);
return;
}
}S;
//--------------------//
int main()
{
scanf("%s",s+1);
int len=strlen(s+1);
for(int i=1;i<=len;i++)
S.insert(s[i]);
S.build();
S.DFS(1);
printf("%lld",S.ans);
return 0;
}
參考資料
\(\mathcal{Alex\_Wei's\ Blog}\)
\(\mathcal{OI-Wiki}\)
總結(jié)
以上是生活随笔為你收集整理的「Note」字符串方向 - 自动机相关的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 深入探讨控制反转(IOC)与依赖注入(D
- 下一篇: 添能天地宝羊奶粉孩子可以喝吗