字尾自動機學習筆記
阿新 • • 發佈:2021-02-15
# 字尾自動機學習筆記
## 作用
字尾自動機$(SAM)$是一個能解決許多字串相關問題的有力的資料結構
它可以把一個字串的所有子串都表示出來
而且從根出發的任意一條合法路徑都是該串中的一個子串
## 構造
要表示一個字串所有的子串,最簡單的方法是對於所有的子串建一棵字典樹
但是這樣做時間複雜度和空間複雜度都是 $O(n^2)$ 的
因此考慮把一些沒有用的節點和邊去掉
定義 $endpos(p)$ 為一個子串 $p$ 出現的所有位置的右端點標號組成的集合
關於 $endpos$ 有如下的性質
$1$、如果兩個子串的 $endpos$ 相同,則其中子串一個必然為另一個的字尾
$2$、對於任意兩個子串 $t$ 和 $p$$( len_t\le len_p)$,要麼$endpos(t)\in endpos(p)$,要麼 $endpos(t) \bigcap endpos(p)=\emptyset$
$3$、對於 $endpos$ 相同的子串,我們將它們歸為一個 $endpos$ 等價類
對於任意一個 $endpos$ 等價類,將包含在其中的所有子串依長度從大到小排序,則每一個子串的長度均為上一個子串的長度減 $1$,且為上一個子串的字尾
簡單來說,一個 $endpos$ 等價類內的串的長度連續
$4$、$endpos$ 等價類個數的級別為 $O(n)$
如果我們在一個字串的前面新增兩個不同的字元
可以把當前的集合分成兩個沒有交集的集合
類似於線段樹的分割方法可以達到最大的劃分數 $2n$
所以後綴自動機的空間要開 $2$ 倍
如果把母集作為分割出來的小集合的父親,就會形成一棵樹,這就是 $parent\ tree$
$parent\ tree$ 上的節點還有一個性質 $minlen[now]=maxlen[fa]+1$
這樣對於每一個 $endpos$ 等價類,只需要記錄屬於它的字串的最大長度即可,最小長度可以由父親節點推出來
字尾自動機是在 $parent$ 樹的基礎上構建而來的,$parent$ 樹的節點就是字尾自動機的節點
當然,字尾自動機不僅有 $parent\ tree$ 的父子關係,也有 $trie$ 樹那樣的轉移邊
沿著一個節點通過一條轉移邊到達另一個節點代表著在當前字串後面新增字元
從 $parent\ tree$ 上的父親節點達到兒子節點則代表著在當前字串前面新增字元
具體構造的時候採用增量法構造
```c++
rg int p=lst;
rg int np=lst=++cnt;
len[np]=len[p]+1;
```
設當前已經構造到了字串的第 $n$ 個字元
首先記錄一下新增第 $n-1$ 個字元時新建的節點 $p$
對於當前的位置新開一個節點 $np$,代表著只含有位置 $n$ 的 $endpos$ 集合
顯然字串 $s[1 \cdots n]$ 在之前一定沒有出現過,所以它一定屬於 $np$ 這個點
所以這個集合中最長的字串是 $s[1 \cdots n]$,也就是 $s[1 \cdots n-1]+1$
```c++
for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
if(!p) fa[np]=1;
```
現在我們要考慮的就是 $s[2 \cdots n],s[3 \cdots n] \cdots s[n \cdots n]$ 屬於哪一個 $endpos$ 集合
因為我們不知道它們在之前有沒有出現過
所以去跳 $np$ 在 $parent\ tree$ 上的父親
含義就是字串 $s[1 \cdots n-1]$ 在前面不斷去掉字元
看 $s[1 \cdots n-1],s[2 \cdots n-1] \cdots s[n-1 \cdots n-1]$ 能不能在後面新增一個字元 $s[n]$到達一個已有的狀態
如果不能,就代表著當前長度以 $n$ 結尾的字串還沒有出現過,那麼它顯然也屬於 $np$ 這個集合
同時由 $p$ 向 $np$ 建一條邊,代表著可以在當前字串的後面新增字元
如果都到根節點了還沒有找到一個之前已經出現過的字串
只能說明 $s[n]$ 這個種類的字元之前沒有出現
直接把它連向根節點就行了
```c++
rg int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
```
否則說明以 $n$ 為結尾的子串之前出現過一部分
這些已經出現過的子串的 $endpos$ 肯定不是單獨的一個 $n$
所以就不能把它歸到 $np$ 這個集合中了
而且我們並不知道這些以 $s[n]$ 結尾的子符串是不是 $s[1 \cdots n]$ 的子串
有可能全部是,也有可能只有一部分是
如果全部是的話,我們就不需要新開一個 $endpos$ 集合了
只要在之前的 $endpos$ 集合的基礎上添一個 $n$ 就行了
否則我們就要把這兩部分分開,一部分含有 $n$,一部分不含有 $n$
判斷的標準就是 $len[q]=len[p]+1$
因為我們由 $p$ 到 $q$ 的含義就是在一個以 $s[n-1]$ 結尾的字串的後面加一個字元變成以 $s[n]$ 結尾的字串
如果出現了 $q$ 中最長字串不是僅僅添加了一個字元得到的情況
說明這個最長字串一定不是以 $s[n]$ 結尾的
反之亦然
如果滿足條件就很好辦了,直接把 $np$ 的父親設為 $q$
而且此時我們不用去跳 $p$ 的父親了
因為 $p$ 的父親節點的出邊指向的節點一定全是以 $s[n]$ 結尾的
而且這些字串一定是 $q$ 中字串的字尾
它們的 $endpos$ 集合都整體加了 $n$
```c++
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
```
如果不滿足呢
肯定要把兩個集合分開
新建一個點 $nq$ 儲存 $endpos$ 多了 $n$ 的字串
顯然這些字串中長度最長的就是 $len[p]+1$
所以 $len[nq]=len[p]+1$
因為我們只是把原來的集合一分為二,所以原來集合的出邊直接讓兩個兒子繼承就行了
但是父子關係要變一下
因為 $nq$ 和 $q$ 是由一個集合分出來的,它們肯定滿足字尾關係
因為 $nq$ 的長度更短並且 $endpos$ 集合中的元素更多
所以要把 $q$ 的父親設為 $nq$
同理 $np$ 的父親也是 $nq$
最後再把本應該指向 $nq$ 的邊改過來就行了
```c++
void insert(rg int c){
rg int p=lst;
rg int np=lst=++cnt;
len[np]=len[p]+1;
for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
if(!p) fa[np]=1;
else {
rg int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else {
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
}
}
siz[np]=1;
}
```
這樣建造出來的字尾自動機是一個$DAG$($AC$自動機則是一個 $Trie$ 圖)
和迴文自動機不同,長度長的不一定編號大,也就是說 $1 \sim n$ 不一定是一個拓撲序
所以還要按照長度進行桶排,得到的才是最終的拓撲序
### 廣義字尾自動機
和普通的字尾自動機同理
加入一個新的字串之前,要把 $lst$ 重置成 $1$
對於之前已經出現過的節點不要重複去建就行了
要注意的是廣義字尾自動機不能按照桶排來確定拓撲序
要用正常的佇列的寫法
```c++
struct SAM{
int ch[maxn][28],fa[maxn],len[maxn],cnt,siz[maxn];
int insert(rg int lst,rg int c){
rg int p=lst;
if(ch[p][c]){
rg int q=ch[p][c];
if(len[q]==len[p]+1) return q;
else {
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
return nq;
}
}
rg int np=++cnt;
len[np]=len[p]+1;
for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
if(!p) fa[np]=1;
else {
rg int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else {
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
}
}
return np;
}
void build(){
cnt=1;
n=read();
for(rg int i=1;i<=n;i++){
scanf("%s",s+1);
rg int nlen=strlen(s+1);
for(rg int lst=1,j=1;j<=nlen;j++){
lst=insert(lst,s[j]-'a'+1);
siz[lst]++;
}
}
}
void calc(){
rg long long ans=0;
for(rg int i=1;i<=cnt;i++){
ans+=len[i]-len[fa[i]];
}
printf("%lld\n",ans);
}
}sam;
```
## 例題
### P3181 [HAOI2016]找相同字元
[題目傳送門](https://www.luogu.com.cn/problem/P3181)
#### 分析
廣義字尾自動機的模板題
對於節點 $i$,它代表的 $endpos$ 集合中本質不同的字串一共有 $len[i]-len[fa[i]]$ 個
對於兩個字串,分別記錄一下它們在某個 $endpos$ 集合中出現的次數
最終的答案就是 $(len[i]-len[fa[i]]) \times siz[i][0] \times siz[i][1]$
#### 程式碼
#include
#include
#include
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=2e6+5;
char s[maxn];
struct SAM{
int ch[maxn][28],fa[maxn],len[maxn],cnt,siz[maxn][2],rd[maxn];
std::queue q;
int insert(rg int lst,rg int c){
rg int p=lst;
if(ch[p][c]){
rg int q=ch[p][c];
if(len[q]==len[p]+1) return q;
else {
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
return nq;
}
}
rg int np=++cnt;
len[np]=len[p]+1;
for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
if(!p) fa[np]=1;
else {
rg int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else {
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
}
}
return np;
}
void build(){
cnt=1;
for(rg int i=0;i<=1;i++){
scanf("%s",s+1);
rg int nlen=strlen(s+1);
for(rg int lst=1,j=1;j<=nlen;j++){
lst=insert(lst,s[j]-'a'+1);
siz[lst][i]++;
}
}
for(rg int i=1;i<=cnt;i++) rd[fa[i]]++;
for(rg int i=1;i<=cnt;i++) if(rd[i]==0) q.push(i);
while(!q.empty()){
rg int now=q.front();
q.pop();
siz[fa[now]][0]+=siz[now][0],siz[fa[now]][1]+=siz[now][1];
--rd[fa[now]];
if(rd[fa[now]]==0) q.push(fa[now]);
}
}
void calc(){
rg long long ans=0;
for(rg int i=1;i<=cnt;i++){
ans+=1LL*(len[i]-len[fa[i]])*siz[i][0]*siz[i][1];
}
printf("%lld\n",ans);
}
}sam;
int main(){
sam.build();
sam.calc();
return 0;
}
```
### P4081 [USACO17DEC]Standing Out from the Herd P
[題目傳送門](https://www.luogu.com.cn/problem/P4081)
#### 分析
用線段樹合併維護每一個 $emdpos$ 集合中有多少個字串出現過
如果只有一種字串出現過就累加答案
注意有些情況下字尾自動機上的線段樹合併和普通的線段樹合併有所不同
普通的線段樹合併會破壞原來兩顆線段樹的形態,想要查詢之前的資訊就不準確了
所以合併的時候要新開一個節點
這道題因為是邊查詢邊統計答案,所以用正常的寫法就行
#### 程式碼
#include
#include
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=1e6+5;
struct trr{
int lch,rch,siz;
}tr[maxn*20];
int rt[maxn],cnt,n;
void push_up(rg int da){
tr[da].siz=tr[tr[da].lch].siz+tr[tr[da].rch].siz;
}
int ad(rg int da,rg int l,rg int r,rg int wz){
if(!da) da=++cnt;
if(l==r){
tr[da].siz=1;
return da;
}
rg int mids=(l+r)>>1;
if(wz<=mids) tr[da].lch=ad(tr[da].lch,l,mids,wz);
else tr[da].rch=ad(tr[da].rch,mids+1,r,wz);
push_up(da);
return da;
}
int bing(rg int aa,rg int bb,rg int l,rg int r){
if(!aa || !bb) return aa+bb;
if(l==r){
tr[aa].siz+=tr[bb].siz;
tr[aa].siz=1;
return aa;
}
rg int mids=(l+r)>>1;
tr[aa].lch=bing(tr[aa].lch,tr[bb].lch,l,mids);
tr[aa].rch=bing(tr[aa].rch,tr[bb].rch,mids+1,r);
push_up(aa);
return aa;
}
int cx(rg int da,rg int l,rg int r){
if(l==r) return l;
rg int mids=(l+r)>>1;
if(tr[tr[da].lch].siz) return cx(tr[da].lch,l,mids);
else return cx(tr[da].rch,mids+1,r);
}
char s[maxn];
struct SAM{
int ch[maxn][28],fa[maxn],cnt,len[maxn],tax[maxn],a[maxn],ans[maxn];
int insert(rg int lst,rg int c){
rg int p=lst;
if(ch[p][c]){
rg int q=ch[p][c];
if(len[q]==len[p]+1) return q;
else {
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
return nq;
}
}
rg int np=++cnt;
len[np]=len[p]+1;
for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
if(!p){
fa[np]=1;
} else {
rg int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else {
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
}
}
return np;
}
void build(){
cnt=1;
n=read();
for(rg int i=1;i<=n;i++){
scanf("%s",s+1);
rg int nlen=strlen(s+1);
for(rg int j=1,lst=1;j<=nlen;j++){
lst=insert(lst,s[j]-'a'+1);
rt[lst]=ad(rt[lst],1,n,i);
}
}
}
void calc(){
for(rg int i=1;i<=cnt;i++) tax[len[i]]++;
for(rg int i=1;i<=cnt;i++) tax[i]+=tax[i-1];
for(rg int i=1;i<=cnt;i++) a[tax[len[i]]--]=i;
for(rg int i=cnt;i>=1;i--){
rg int p=a[i];
if(tr[rt[p]].siz==1){
ans[cx(rt[p],1,n)]+=len[p]-len[fa[p]];
}
rt[fa[p]]=bing(rt[fa[p]],rt[p],1,n);
}
for(rg int i=1;i<=n;i++){
printf("%d\n",ans[i]);
}
}
}sam;
int main(){
sam.build();
sam.calc();
return 0;
}
```
### P4770 [NOI2018] 你的名字
[題目傳送門](https://www.luogu.com.cn/problem/P4770)
#### 分析
給你一個字串 $S$, 有很多組詢問, 每次給定一個 $T$, 求 $T$ 中不在 $S[l:r]$ 中出現的本質不同的子串個數
用 $T$ 的本質不同的子串減去 $T$ 在 $S[l:r]$ 中出現的本質不同的子串個數
前者很好求,對於 $T$ 建出字尾自動機,答案就是 $\sum len[i]-len[fa[i]]$
關鍵在於如何求出後面的部分
先考慮最簡單的匹配問題
即對於一個字串 $T$,求出它與另一個字串 $S$ 的最長公共子串
對於 $S$ 建立字尾自動機,把初始的位置置為根節點
對於 $T$ 從第一個字元開始列舉
如果字尾自動機的節點上有當前字元的出邊就一直走下去,同時把匹配長度加一
否則就一直跳 $parent\ tree$ 直到匹配上為止,把匹配長度置為當前節點的長度
如果到了根節點還匹配不上,就把匹配長度置為 $0$
```c++
now=1,cs=0;
for(rg int i=1;i<=n;i++){
rg int p=s[i]-'a'+1;
while(now && !ch[now][p]) now=fa[now],cs=len[now];
if(!now){
now=1;
cs=0;
} else {
cs++;
now=ch[now][p];
}
ans=std::max(ans,cs);
}
```
現在無非是在普通匹配的基礎上加上了 $[l,r]$ 的限制
只要用線段樹合併維護一下 $endpos$ 集合即可,這裡要寫新開節點的那一種
線上段樹上查詢當前的節點在 $[l,r]$ 中能匹配的最靠右的端點
如果一直不存在就一直向上跳
跳到第一個合法的位置之後還要繼續向上跳
因為越往上 $endpos$ 集合中含有的元素越多
查詢右端點時得到的答案也就越靠右
一直跳到當前節點的長度限制答案為止
還有一個問題就是如何去重
因為不同位置本質相同的字串只算一次
這時候我們就需要對於 $T$ 建一個字尾自動機
這樣就可以求出以每一個節點為結尾的第一次出現的字串的個數
每一位匹配的長度不要超過這個限度即可
#### 程式碼
```c++
#include
#include
#include
#include
#define rg register
const int maxn=1e6+5;
char s[maxn];
int n,t,rt[maxn],trcnt,l,r;
struct trr{
int lch,rch,mmax;
}tr[maxn*20];
void push_up(rg int da){
tr[da].mmax=std::max(tr[tr[da].lch].mmax,tr[tr[da].rch].mmax);
}
int ad(rg int da,rg int l,rg int r,rg int wz){
da=++trcnt;
if(l==r){
tr[da].mmax=wz;
return da;
}
rg int mids=(l+r)>>1;
if(wz<=mids) tr[da].lch=ad(tr[da].lch,l,mids,wz);
else tr[da].rch=ad(tr[da].rch,mids+1,r,wz);
push_up(da);
return da;
}
int bing(rg int aa,rg int bb,rg int l,rg int r){
if(!aa || !bb) return aa+bb;
rg int cc=++trcnt,mids=(l+r)>>1;
if(l==r){
tr[cc].mmax=std::max(tr[aa].mmax,tr[bb].mmax);
return cc;
}
tr[cc].lch=bing(tr[aa].lch,tr[bb].lch,l,mids);
tr[cc].rch=bing(tr[aa].rch,tr[bb].rch,mids+1,r);
push_up(cc);
return cc;
}
int cx(rg int da,rg int l,rg int r,rg int L,rg int R){
if(!da) return -1;
if(l>=L && r<=R) return tr[da].mmax;
rg int nans=-1,mids=(l+r)>>1;
if(L<=mids) nans=std::max(nans,cx(tr[da].lch,l,mids,L,R));
if(R>mids) nans=std::max(nans,cx(tr[da].rch,mids+1,r,L,R));
return nans;
}
struct SAM{
int len[maxn],ch[maxn][28],fa[maxn],lst,cnt,id[maxn],tax[maxn],nlen,jl[maxn];
void insert(rg int c){
rg int p=lst;
rg int np=lst=++cnt;
len[np]=len[p]+1;
for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
if(!p) fa[np]=1;
else {
rg int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else {
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
}
}
}
void build(rg int op){
for(rg int i=1;i<=cnt;i++) fa[i]=0,memset(ch[i],0,sizeof(ch[i]));
lst=cnt=1;
nlen=strlen(s+1);
for(rg int i=1;i<=nlen;i++){
insert(s[i]-'a'+1);
jl[i]=len[fa[lst]];
if(!op) rt[lst]=ad(rt[lst],1,n,i);
}
if(!op){
for(rg int i=1;i<=cnt;i++) tax[len[i]]++;
for(rg int i=1;i<=nlen;i++) tax[i]+=tax[i-1];
for(rg int i=1;i<=cnt;i++) id[tax[len[i]]--]=i;
for(rg int i=cnt;i>=1;i--){
rg int tmp=id[i];
if(fa[tmp]) rt[fa[tmp]]=bing(rt[fa[tmp]],rt[tmp],1,n);
}
}
}
}sam1,sam2;
long long solve(rg int l,rg int r){
rg long long ans=0;
for(rg int i=1;i<=sam2.cnt;i++) ans+=sam2.len[i]-sam2.len[sam2.fa[i]];
rg int now=1,cs=0,tmp,nrt,nans=0;
for(rg int i=1;i<=sam2.nlen;i++){
rg int p=s[i]-'a'+1;
tmp=cx(rt[sam1.ch[now][p]],1,n,l,r);
while(now && tmp==-1){
now=sam1.fa[now],cs=sam1.len[now];
tmp=cx(rt[sam1.ch[now][p]],1,n,l,r);
}
if(!now) now=1,cs=0;
else {
now=sam1.ch[now][p],cs++,nrt=sam1.fa[now],nans=std::min(sam1.len[now],tmp-l+1);
while(nrt){
tmp=cx(rt[nrt],1,n,l,r);
nans=std::max(nans,std::min(tmp-l+1,sam1.len[nrt]));
if(tmp-l+1>=sam1.len[nrt]) break;
nrt=sam1.fa[nrt];
}
nans=std::min(nans,cs);
if(nans>sam2.jl[i]) ans-=(nans-sam2.jl[i]);
}
}
return ans;
}
int main(){
scanf("%s",s+1);
n=strlen(s+1);
sam1.build(0);
scanf("%d",&t);
rg int l,r;
for(rg int i=1;i<=t;i++){
scanf("%s%d%d",s+1,&l,&r);
sam2.build(1);
printf("%lld\n",solve(l,r));
}
return 0;
}
```
### CF666E Forensic Examination
[題目傳送門](https://www.luogu.com.cn/problem/CF666E)
#### 分析
對於字串陣列建立廣義字尾自動機
查詢之前先對於串 $S$ 在後綴自動上跑一遍匹配
對於每一個位置記錄以該位置結尾的字尾在後綴自動機上能夠匹配的最長的位置以及對應的節點
對於每一個 $endpos$ 集合用線段樹記錄一下它在哪些子串中出現過以及出現的次數
如果要查詢 $s[l,r]$ 在字串組中的那一個字串中出現的次數最多
從預處理出來的以 $r$ 結尾的字尾能夠匹配的最長的位置對應的節點進行樹上倍增
找到第一個長度大於等於 $r-l+1$ 的位置
此時這個位置所含有的字串的種類一定是最多的
直接在對應的線段樹上查詢即可
#### 程式碼
#include
#include
#include
#include
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=1e6+5;
struct trr{
int lch,rch,val,jl;
}tr[maxn*20];
int trcnt,rt[maxn];
void push_up(rg int da){
if(tr[tr[da].lch].val>1;
if(wz<=mids) tr[da].lch=ad(tr[da].lch,l,mids,wz);
else tr[da].rch=ad(tr[da].rch,mids+1,r,wz);
push_up(da);
return da;
}
int bing(rg int aa,rg int bb,rg int l,rg int r){
if(!aa || !bb) return aa+bb;
rg int cc=++trcnt,mids=(l+r)>>1;
if(l==r){
tr[cc].val=tr[aa].val+tr[bb].val;
tr[cc].jl=l;
return cc;
}
tr[cc].lch=bing(tr[aa].lch,tr[bb].lch,l,mids);
tr[cc].rch=bing(tr[aa].rch,tr[bb].rch,mids+1,r);
push_up(cc);
return cc;
}
int cx(rg int da,rg int l,rg int r,rg int L,rg int R){
if(!da) return 0;
if(l>=L && r<=R) return da;
rg int mids=(l+r)>>1,nans=0,tmp;
if(L<=mids){
tmp=cx(tr[da].lch,l,mids,L,R);
if(tr[tmp].val>tr[nans].val) nans=tmp;
else if(tr[tmp].val==tr[nans].val){
if(tr[tmp].jl mids){
tmp=cx(tr[da].rch,mids+1,r,L,R);
if(tr[tmp].val>tr[nans].val) nans=tmp;
else if(tr[tmp].val==tr[nans].val){
if(tr[tmp].jl q;
int insert(rg int lst,rg int c){
rg int p=lst;
if(ch[p][c]){
rg int q=ch[p][c];
if(len[q]==len[p]+1) return q;
else {
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
return nq;
}
}
rg int np=++cnt;
len[np]=len[p]+1;
for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
if(!p) fa[np]=1;
else {
rg int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else {
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
}
}
return np;
}
void build(){
cnt=1;
for(rg int i=1;i<=m;i++){
scanf("%s",s2+1);
nlen=strlen(s2+1);
for(rg int j=1,lst=1;j<=nlen;j++){
lst=insert(lst,s2[j]-'a'+1);
rt[lst]=ad(rt[lst],1,m,i);
}
}
for(rg int i=1;i<=cnt;i++) rd[fa[i]]++,zx[i][0]=fa[i];
for(rg int i=1;i<=cnt;i++) if(rd[i]==0) q.push(i);
while(!q.empty()){
rg int now=q.front();
q.pop();
sta[++tp]=now;
rt[fa[now]]=bing(rt[fa[now]],rt[now],1,m);
rd[fa[now]]--;
if(rd[fa[now]]==0) q.push(fa[now]);
}
for(rg int i=tp;i>=1;i--){
rg int now=sta[i];
for(rg int j=1;j<=20;j++){
zx[now][j]=zx[zx[now][j-1]][j-1];
}
}
}
void pre(){
rg int cs=0,now=1;
for(rg int i=1;i<=n;i++){
rg int p=s1[i]-'a'+1;
while(now && !ch[now][p]) now=fa[now],cs=len[now];
if(!now) now=1,cs=0;
else now=ch[now][p],cs++;
mat[i]=now,mmax[i]=cs;
}
}
void solve(rg int l1,rg int r1,rg int l2,rg int r2){
if(mmax[r2]=0;i--){
if(len[zx[now][i]]>=r2-l2+1) now=zx[now][i];
}
if(len[now]
### P5212 SubString
[題目傳送門](https://www.luogu.com.cn/problem/P5212)
#### 分析
一個字串出現的次數就是它子樹內的權值和
強制線上要用 $lct$ 維護
因為子樹和不好維護
所以可以在每一次修改後把當前節點到根的路徑都加上對應的權值
查詢的時候只要單點查詢就行了
#### 程式碼
#include
#include
#include
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=3e6+5;
char s[maxn];
std::string chars;
int mask;
void getit(int mask){
scanf("%s",s);
chars=s;
for(int j=0;j=1;i--) push_down(sta[i]);
while(!isroot(x)){
rg int y=fa[x];
rg int z=fa[y];
if(!isroot(y)){
(ch[z][1]==y)^(ch[y][1]==x)?xuanzh(x):xuanzh(y);
}
xuanzh(x);
}
}
void access(rg int x){
for(rg int y=0;x;y=x,x=fa[x]){
splay(x);
ch[x][1]=y;
}
}
void makeroot(rg int x){
access(x);
splay(x);
rev[x]^=1;
push_down(x);
}
int findroot(rg int x){
access(x);
splay(x);
push_down(x);
while(ch[x][0]){
x=ch[x][0];
push_down(x);
}
splay(x);
return x;
}
void split(rg int x,rg int y){
makeroot(x);
access(y);
splay(y);
}
void link(rg int x,rg int y){
makeroot(x);
if(findroot(y)!=x) fa[x]=y;
}
void cut(rg int x,rg int y){
makeroot(x);
if(findroot(y)==x && fa[y]==x && ch[y][0]==0){
ch[x][1]=fa[y]=0;
}
}
}lct;
struct SAM{
int ch[maxn][28],fa[maxn],lst,len[maxn],cnt;
void insert(rg int c){
rg int p=lst;
rg int np=lst=++cnt;
len[np]=len[p]+1;
for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
if(!p){
fa[np]=1;
lct.link(np,fa[np]);
} else {
rg int q=ch[p][c];
if(len[q]==len[p]+1){
fa[np]=q;
lct.link(np,fa[np]);
} else {
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
lct.cut(q,fa[q]);
lct.link(nq,fa[q]);
lct.link(np,nq);
lct.link(q,nq);
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
lct.splay(q);
lct.sum[nq]=lct.sum[q];
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
}
}
lct.split(np,1);
lct.sum[1]++;
lct.tag[1]++;
}
void build(){
lst=cnt=1;
scanf("%s",s+1);
rg int nlen=strlen(s+1);
for(int i=1;i<=nlen;i++){
insert(s[i]-'A');
}
}
void ad(){
rg int nlen=chars.length();
for(rg int i=0;i
### P4248 [AHOI2013]差異
[題目傳送門](https://www.luogu.com.cn/problem/P4248)
#### 分析
在後綴自動機中兩個字串中的 $lcp$ 就是它們在 $fail$ 樹上的最近公共祖先
題目給出的式子其實就是兩兩之間的路徑長度
列舉每一條邊的貢獻加起來即可
#### 程式碼
#include
#include
#define rg register
const int maxn=4e6+5;
char s[maxn];
long long ans;
struct SAM{
int ch[maxn][28],fa[maxn],lst,cnt,len[maxn],a[maxn],tax[maxn],siz[maxn],n;
void insert(rg int c){
rg int p=lst;
rg int np=lst=++cnt;
len[np]=len[p]+1;
for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
if(!p) fa[np]=1;
else {
rg int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else {
rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
}
}
siz[np]=1;
}
void build(){
scanf("%s",s+1);
n=strlen(s+1);
std::reverse(s+1,s+1+n);
lst=cnt=1;
for(rg int i=1;i<=n;i++) insert(s[i]-'a'+1);
}
void calc(){
for(rg int i=1;i<=cnt;i++) tax[len[i]]++;
for(rg int i=1;i<=cnt;i++) tax[i]+=tax[i-1];
for(rg int i=1;i<=cnt;i++) a[tax[len[i]]--]=i;
for(rg int i=cnt;i>=1;i--){
rg int p=a[i];
siz[fa[p]]+=siz[p];
ans+=1LL*(len[p]-len[fa[p]])*siz[p]*(n-siz[p]);
}
printf("%lld\n",ans);
}
}sam;
int main(){
sam.build();
sam.calc();
return 0;
}
```