前言:菜死了,菜死了。
插入模板:
struct SAM{
int last,cnt,ch[N][26],fa[N],sz[N];
void ins(int c){
int p=last,np=++cnt;len[np]=len[p]+1;
while(;p&&!ch[p][c];p=fa[p]) ch[p][c]=np;
if(!p) fa[np]=1;
else {
int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else { int nq=++cnt;
len[nq]=len[p]+1;
memcyp(ch[nq],ch[q],sizeof ch[q]);
fa[nq]=fa[q],fa[q]=fa[p]=nq;
while(;ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
}
}sz[np]=1;
}
};
拓扑模板:
for(int i=1;i<=cnt;i++) b[len[i]]++;
for(int i=1;i<=cnt;i++) b[i]+=b[i-1];
for(int i=1;i<=cnt;i++) a[b[len[i]]--]=i;
for(int i=cnt;i;i--){
int p=a[i];
// dp[p]=1;for(int j=0;j<26;j++) if(ch[p][j]) dp[p]+=dp[ch[p][j]]; 求从i开始自动机的子串个数
// sz[fa[p]]+=sz[p]; 求endpos 集合子串出现次数
}
1.求本质不同的子串个数。
法1:考虑拓扑排序后 d p dp dp,因为 S A M SAM SAM上从任意点到任意点都是原串的子串。
所以令 d p [ i ] dp[i] dp[i]表示从第 i i i个状态出发的本质不同子串数,然后根据边转移即可。
for(int i=1;i<=cnt;i++) b[len[i]]++;
for(int i=1;i<=cnt;i++) b[i]+=b[i-1];
for(int i=1;i<=cnt;i++) a[b[len[i]]--]=i;
for(int i=cnt;i;i--){
int p=a[i];
dp[p]=1;
for(int i=0;i<26;i++) if(ch[p][i]) dp[p]+=dp[ch[p][i]];
}
printf("%lld\n",dp[1]-1);
法2:考虑每次添加一个字符增加的子串数,显然对于当前状态 i i i,属于 e n d p o s i endpos_i endposi这个集合的子串就是增加的子串数。因为这个集合的子串长度都是连续且唯一的,长度变化从 [ m i n l e n , m a x l e n ] [minlen,maxlen] [minlen,maxlen],令 l e n [ i ] = m a x l e n len[i]=maxlen len[i]=maxlen,显然 m i n l e n = l e n [ f a [ i ] ] + 1 minlen=len[fa[i]]+1 minlen=len[fa[i]]+1,所以增加的子串数位: m a x l e n − m i n l e n + 1 = l e n [ i ] − l e n [ f a [ i ] ] maxlen-minlen+1=len[i]-len[fa[i]] maxlen−minlen+1=len[i]−len[fa[i]]。
最终求和即可。
for(int i=1;i<=cnt;i++) ans+=len[i]-len[fa[i]];
printf("%lld\n",ans);
2.求长度为 i i i的子串出现的最大次数。
思路:因为每个状态
e
n
d
p
o
s
endpos
endpos集合的子串的出现次数是相同的,我们可以用拓扑
d
p
dp
dp,对应状态子串集合的出现次数。
然后用一个数组
a
n
s
[
]
,
a
n
s
[
l
e
n
[
i
]
=
m
a
x
(
a
n
s
[
l
e
n
[
i
]
,
s
z
[
i
]
)
ans[],ans[len[i]=max(ans[len[i],sz[i])
ans[],ans[len[i]=max(ans[len[i],sz[i]),来更新长度
i
i
i对应的最长的子串的最大出现次数。
又因为前一个串是后一个串的后缀,所以最后我们还需倒着更新一下
a
n
s
[
i
]
=
m
a
x
(
a
n
s
[
i
]
,
a
n
s
[
i
+
1
]
)
ans[i]=max(ans[i],ans[i+1])
ans[i]=max(ans[i],ans[i+1])。
for(int i=1;i<=cnt;i++) b[len[i]]++;
for(int i=1;i<=cnt;i++) b[i]+=b[i-1];//求前缀和.
for(int i=1;i<=cnt;i++) a[b[len[i]]--]=i; //类似桶排 将结点长度从小到大排序.
for(int i=cnt;i;i--){ //从长度大的开始遍历,跑拓扑序.
int p=a[i]; //因为长度大的结点的fail结点指向的后缀也是长度大的后缀
sz[fa[p]]+=sz[p];//状态转移.
// if(sz[p]>1) ans=max(ans,1LL*sz[p]*len[p]);//更新答案.
}
for(int i=1;i<=cnt;i++) ans[len[i]]=max(ans[len[i]],sz[i]);
for(int i=n-1;i;i--) ans[i]=max(ans[i],ans[i+1]);
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
3.求模式串是否在文本串出现过。
用文本串建立 S A M SAM SAM,然后模式串跑 S A M SAM SAM即可。
scanf("%s",t+1);int p=1;
for(int i=1;t[i];i++){
int c=t[i]-'a';
if(ch[p][c]) p=ch[p][c];
else return false;
}return true;
}
4.求两个字符串的最长公共子串长度。
思路:一个字符串在另一个字符串上跑 S A M SAM SAM,每次更新最大长度。
int ans=0,p=1,res=0;
for(int i=1;b[i];i++){
int x=b[i]-'a';
if(ch[p][x]) p=ch[p][x],ans++;
else {
while(p&&!ch[p][x]) p=fa[p];
if(!p) p=1,ans=0;
else ans=len[p]+1,p=ch[p][x];
}
if(res<ans) res=ans;//以b[i]结尾子串的最大LCM长度
}
printf("%d\n",res);
5.求字典序第 k k k小的子串。
s z [ i ] sz[i] sz[i] 为结点 i i i对应的 e n d p o s endpos endpos集合的每个子串的出现次数。
s u m [ i ] sum[i] sum[i] 从 i i i出发的自动机的子串数量。
思路:1.若子串为本质不同,则
s
z
[
i
]
=
1
sz[i]=1
sz[i]=1,每个状态对应的
e
n
d
p
o
s
endpos
endpos集合的子串出现次数为
1
1
1。
2.否则,按长度状态转移求出
s
z
[
]
sz[]
sz[]。
if(!t) for(int i=cnt;i;i--) sum[i]=sz[i]=1;//sz[1]=0; 本质不同的子串
else for(int i=cnt;i;i--) sum[i]=sz[i];
sum[1]=sz[1]=0; //位置不同也算不同
然后令数组 s u m [ i ] sum[i] sum[i]表示从 i i i出发的子串个数。
for(int i=cnt;i;i--)
for(int j=0;j<26;j++) if(ch[a[i]][j]) sum[a[i]]+=sum[ch[a[i]][j]];
然后跑 d f s dfs dfs。
void Print(int p,int k){
if(k<=sz[p]) return;
k-=sz[p];
for(int i=0;i<26;i++)
if(ch[p][i]){
int q=ch[p][i];
if(k>sum[q]) k-=sum[q];
else {
putchar('a'+i),Print(q,k);return;
}
}
}