鸽了一年的总结x,很早就写好了,但是懒得传
SAM总结
后缀自动机是一种无比强大的数据结构,可以处理绝大多数字符串问题,本质上是一个字符串所有子串压缩后的自动机,下面将根据题目讲解sam的性质和应用
一.初级应用
1.多文本匹配,直接跑就好了, O ( n + m ) O(n+m) O(n+m)
2.P2408不同子串个数
题意:
两种思路
1. s a m sam sam每个结点的状态就包含了部分子串,每个状态的贡献是 l e n [ i ] − l e n [ l i n k [ i ] ] len[i]-len[link[i]] len[i]−len[link[i]]
2. s a m sam sam从根节点出发到每个状态的每个路径代表一个独一无二的子串。而且 s a m sam sam是个 D A G DAG DAG,任意结点出发路径组成串,互不相同, 所以可以直接 d p dp dp记搜, d p [ u ] dp[u] dp[u]代表从 u u u出发的路径数,所以按拓扑序 d p [ u ] = d p [ v ] + 1 dp[u]=dp[v]+1 dp[u]=dp[v]+1
复杂度都是 O ( n ) O(n) O(n)
下面两种思路都提供了代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e5+5;
struct SAM{
int len[maxn],link[maxn],ch[maxn][26],tot,last;
ll ans1[maxn];
void init(){
tot=last=1;
}
void extend(int c){
int cur=++tot,p=last;last=cur;
len[cur]=len[p]+1;
for(;p&&!ch[p][c];p=link[p])ch[p][c]=cur;
if(!p)link[cur]=1;
else{
int q=ch[p][c];
if(len[p]+1==len[q])link[cur]=q;
else{
int clone=++tot;
len[clone]=len[p]+1;
memcpy(ch[clone],ch[q],sizeof(ch[q]));
link[clone]=link[q];link[q]=link[cur]=clone;
for(;p&&ch[p][c]==q;p=link[p])ch[p][c]=clone;
}
}
}
ll solve1(int x){
if(ans1[x])return ans1[x];
for(int i=0;i<26;++i){
if(ch[x][i])ans1[x]+=solve1(ch[x][i])+1;
}
return ans1[x];
}
ll solve2(){
ll ans=0;
for(int i=2;i<=tot;++i)ans+=len[i]-len[link[i]];
return ans;
}
}sam;
char s[maxn];
int main(){
int len;
scanf("%d",&len);
scanf("%s",s+1);
sam.init();
for(int i=1;i<=len;++i)sam.extend(s[i]-'a');
cout<<sam.solve1(1);
//cout<<sam.solve2();
return 0;
}
3.P3804 模板后缀自动机
题意:求字符串出现次数不为 1 的子串的出现次数乘上该子串长度的最大值。
思路:
其实就是求个
e
n
d
p
o
s
endpos
endpos集合,对每个前缀所属点的
s
z
sz
sz设为1,这些点是前缀的
e
d
p
edp
edp出现的最深的点,然后
l
e
n
len
len基数排序或者
d
f
s
dfs
dfs即可
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e6+5;
struct SAM{
int len[maxn],link[maxn],ch[maxn][26],last,tot,sz[maxn],tax[maxn];
int rk[maxn];
void init(){ //一定记得
tot=last=1;
}
void extend(int c){
int cur=++tot,p=last;last=cur;sz[cur]=1;
len[cur]=len[p]+1;
for(;p&&!ch[p][c];p=link[p])ch[p][c]=cur;
if(!p)link[cur]=1;
else{
int q=ch[p][c];
if(len[q]==len[p]+1)link[cur]=q;
else{
int clone=++tot;
memcpy(ch[clone],ch[q],sizeof(ch[q]));
len[clone]=len[p]+1;
link[clone]=link[q],link[q]=link[cur]=clone;
for(;p&&ch[p][c]==q;p=link[p])ch[p][c]=clone;
}
}
}
ll claSZ(int x){
ll ans=0;
for(int i=1;i<=tot;++i)tax[len[i]]++;
for(int i=1;i<=x;++i)tax[i]+=tax[i-1];
for(int i=1;i<=tot;++i)rk[tax[len[i]]--]=i;
for(int i=tot;i>=1;--i){
int now=rk[i];
sz[link[now]]+=sz[now];
if(sz[now]>1)ans=max(ans,(ll)sz[now]*len[now]);
}
return ans;
}
}sam;
char s[maxn];
int main(){
scanf("%s",s+1);
int len=strlen(s+1);
sam.init();
for(int i=1;i<=len;++i)sam.extend(s[i]-'a');
cout<<sam.claSZ(len);
}
4.CFF802 I Fake News(hard)
题意:本质不同子串出现次数的平方和
思路:求出 e n d p o s endpos endpos集合大小后乱做
#include<bits/stdc++.h>
using namespace std;
using ll=long long ;
const int maxn=2e5+5;
struct SAM{
int len[maxn],sz[maxn],tot,link[maxn],ch[maxn][26],last;
int rk[maxn],tax[maxn];
void init(int x){
for(int i=0;i<=x;++i)tax[i]=0;
for(int i=1;i<=tot;++i)sz[i]=0;
for(int i=1;i<=tot;++i)
for(int j=0;j<=26;++j)ch[i][j]=0;
tot=last=1;
}
void extend(int c){
int cur=++tot,p=last;last=cur;sz[cur]=1;
len[cur]=len[p]+1;
for(;p&&!ch[p][c];p=link[p])ch[p][c]=cur;
if(!p)link[cur]=1;
else{
int q=ch[p][c];
if(len[q]==len[p]+1)link[cur]=q;
else{
int clone=++tot;
len[clone]=len[p]+1;
memcpy(ch[clone],ch[q],sizeof(ch[q]));
link[clone]=link[q];link[q]=link[cur]=clone;
for(;p&&ch[p][c]==q;p=link[p])ch[p][c]=clone;
}
}
}
void calSZ(int x){
for(int i=1;i<=tot;++i)tax[len[i]]++;
for(int i=1;i<=x;++i)tax[i]+=tax[i-1];
for(int i=1;i<=tot;++i)rk[tax[len[i]]--]=i;
for(int i=tot;i>=1;--i){
int now=rk[i];
sz[link[now]]+=sz[now];
}
}
ll solve(){
ll ans=0;
for(int i=2;i<=tot;++i){
ans+=(ll)sz[i]*sz[i]*(len[i]-len[link[i]]);
}
return ans;
}
}sam;
char s[maxn];
int main(){
int t;
scanf("%d",&t);
while(t--){
scanf("%s",s+1);
int len=strlen(s+1);
sam.init(len);
for(int i=1;i<=len;++i)sam.extend(s[i]-'a');
sam.calSZ(len);
cout<<sam.solve()<<"\n";
}
return 0;
}
5.bzoj4516 生成魔咒
题意:
每次 p b pb pb字符,统计本质不同子串数。
思路:
推导一下就知道,其实就是
+
=
l
e
n
[
i
]
−
l
e
n
[
l
i
n
k
[
i
]
]
+=len[i]-len[link[i]]
+=len[i]−len[link[i]],实际上根据定义也知道并没有变化。
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int maxn=2e5+5;
struct SAM{
int len[maxn],link[maxn],last,tot;
map<int,int>ch[maxn];
ll ans;
SAM(){
tot=last=1;
ans=0;
}
void extend(int c){
int cur=++tot,p=last;last=cur;
len[cur]=len[p]+1;
for(;p&&ch[p].find(c)==ch[p].end();p=link[p])ch[p][c]=cur;
if(!p)link[cur]=1;
else{
int q=ch[p][c];
if(len[p]+1==len[q])link[cur]=q;
else{
int clone=++tot;
ch[clone]=ch[q];
len[clone]=len[p]+1;
link[clone]=link[q];link[q]=link[cur]=clone;
for(;p&&ch[p][c]==q;p=link[p])ch[p][c]=clone;
}
}
ans+=len[cur]-len[link[cur]];
}
}sam;
int main(){
int n;
scanf("%d",&n);
int x;
for(int i=1;i<=n;++i){
scanf("%d",&x);
sam.extend(x);
cout<<sam.ans<<"\n";
}
return 0;
}
6.字典序第k小子串
题意:
长度为 n n n的字符串,求第 k k k小子串是什么
思路:
s
u
m
[
i
]
sum[i]
sum[i]表示经过从某字符出发 经过i结点的子串数量 ,通过拓扑序
d
p
dp
dp即可求得,
t
=
0
/
1
t=0/1
t=0/1只是决定了
s
u
m
[
i
]
sum[i]
sum[i]的初始值,以及每个点要减去的大小而已。
在 s a m sam sam上 d f s dfs dfs,从小到大枚举要转移的点,假如小于 k k k就不转移减掉即可。一旦当前 k < = s z [ x ] k<=sz[x] k<=sz[x]就表明找到了
复杂度 O ( n ) O(n) O(n)
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int maxn=1e6+5;
int t,k;
struct SAM{
int len[maxn],tot,last,sz[maxn],link[maxn],ch[maxn][26],tax[maxn],rk[maxn],cnt;
ll sum[maxn];
char ans[maxn];
SAM(){
tot=last=1;
}
void extend(int c){
int cur=++tot,p=last;last=cur;sz[cur]=1;
len[cur]=len[p]+1;
for(;p&&!ch[p][c];p=link[p])ch[p][c]=cur;
if(!p)link[cur]=1;
else{
int q=ch[p][c];
if(len[q]==len[p]+1)link[cur]=q;
else{
int clone=++tot;
memcpy(ch[clone],ch[q],sizeof(ch[q]));
len[clone]=len[p]+1;
link[clone]=link[q];link[cur]=link[q]=clone;
for(;p&&ch[p][c]==q;p=link[p])ch[p][c]=clone;
}
}
}
void calSZ(int x){
for(int i=1;i<=tot;++i)tax[len[i]]++;
for(int i=1;i<=x;++i)tax[i]+=tax[i-1];
for(int i=1;i<=tot;++i)rk[tax[len[i]]--]=i;
for(int i=tot;i;--i){
int now=rk[i];
sz[link[now]]+=sz[now];
}
sz[1]=0;
}
void calSum(){//sum[i] sam上经过从某字符出发 经过i结点的子串数量
//看题意initsum
for(int i=2;i<=tot;++i)sum[i]=sz[i];
for(int i=tot;i>=1;--i)
for(int j=0;j<26;++j){
int now=rk[i];//记得calSZ先算rk
if(ch[now][j])sum[now]+=sum[ch[now][j]];
}
}
void solve(int x,int k){
if(k<=sz[x])return;
k-=sz[x];
for(int i=0;i<26;++i){
if(!ch[x][i])continue;
if(sum[ch[x][i]]<k){ k-=sum[ch[x][i]];continue;}
putchar(i+'a');
solve(ch[x][i],k);
return;
}
}
}sam;
char s[maxn];
int main(){
scanf("%s",s+1);
int len=strlen(s+1);
for(int i=1;i<=len;++i)sam.extend(s[i]-'a');
scanf("%d%d",&t,&k);
sam.calSZ(len);
if(!t)
for(int i=2;i<=sam.tot;++i)sam.sz[i]=1;
sam.calSum();
if(sam.sum[1]<k)puts("-1");
else sam.solve(1,k);
}
二.SAM与AC自动机相似之处
我们都知道 A C AC AC自动机可以用于多模式串匹配问题,事实上 S A M SAM SAM与其极为相似, S A M SAM SAM Parent树上父亲结点也是子结点字符不断删去前缀而成, p a r e n t parent parent树的儿子被匹配了,则说明其祖先其祖先的某个子串也被匹配导。 S A M SAM SAM可以看成是对某个串所有子串建 A C AC AC自动机
1.SP1811 LCS
题意:两串最长公共子串
思路:
对其中一个串建 S A M SAM SAM, 另一个串暴力匹配即可,每个状态的最大值相当于对每个前缀求了个最长后缀公共子串
2.SP1812 LCS2
题意:求多串公共子串
思路:
对第一个串建 S A M SAM SAM, 然后剩下的串都在上面匹配,维护每个串在每个状态能匹配的最大值,注意得在Parents树上把对祖先的影响也算进去,最后取所有串在每个状态的最小值的最大值即可
#include<bits/stdc++.h>
#define eb emplace_back
using namespace std;
const int maxn=2e5+5;
struct SAM{//maxn开2倍
int len[maxn],link[maxn],ch[maxn][26],last,tot,mx[maxn],mn[maxn],ans;//len指状态内最长长度
vector<int>G[maxn];
SAM(){ //link指向状态内最长字符串的最长的一个在另一个endpos类的后缀
tot=last=1;//sz endpos大小
}
void extend(int c){
int cur=++tot,p=last;last=cur;
len[cur]=len[p]+1;
for(;p&&!ch[p][c];p=link[p])ch[p][c]=cur;
if(!p)link[cur]=1;
else{
int q=ch[p][c];
if(len[p]+1==len[q])link[cur]=q;
else{
int clone=++tot;//==len[p]+1的复制出来
memcpy(ch[clone],ch[q],sizeof(ch[q]));
len[clone]=len[p]+1;
link[clone]=link[q];link[q]=link[cur]=clone;
for(;p&&ch[p][c]==q;p=link[p])ch[p][c]=clone;
}
}
}
void dfs(int x){
for(auto&v:G[x]){
dfs(v);
mx[x]=max(mx[x],min(mx[v],len[x]));
}
mn[x]=min(mn[x],mx[x]);
}
void init(){
for(int i=1;i<=tot;++i)mn[i]=1e9;
for(int i=2;i<=tot;++i)G[link[i]].eb(i);
}
void query(char*s){
int L=strlen(s+1),p=1,nowlen=0;
for(int i=1;i<=L;++i){
int c=s[i]-'a';
if(ch[p][c]){
p=ch[p][c];nowlen++;
}else{
while(p&&!ch[p][c])p=link[p];
if(!p)p=1,nowlen=0;
else nowlen=len[p]+1,p=ch[p][c];
}
mx[p]=max(nowlen,mx[p]);
}
dfs(1);
for(int i=1;i<=tot;++i)mx[i]=0;
}
void solve(){
for(int i=1;i<=tot;++i){
if(mn[i]!=1e9)ans=max(ans,mn[i]);
}
cout<<ans;
}
}sam;
char s[maxn],s1[maxn];
int ans,num=0;
int main(){
scanf("%s",s+1);
int L=strlen(s+1);
for(int i=1;i<=L;++i)sam.extend(s[i]-'a');
sam.init();
while(~scanf("%s",s+1))
sam.query(s);
sam.solve();
return 0;
}
3.cf235C
题意:给出一个文本串和若干询问串,求每个询问串的所有本质不同循环,能够匹配到的次数。
思路:
我们先不考虑本质不同,会发现循环这种操作与后缀自动机的相关性,前面删除一个字母相当于是往parents树上跳 l i n k link link,后面加一个字母相当于继续匹配,所以匹配完整个字符,再 O ( n ) O(n) O(n)扫一遍即可,由于匹配的 l e n len len是固定的,所以本质不同只需要在结点上打标记即可。
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
using ll=long long;
const int maxn=2e6+5;
vector<int>G[maxn];
struct SAM{//maxn开2倍
int len[maxn],link[maxn],ch[maxn][26],last,tot,sz[maxn],num[maxn],cnt;//len指状态内最长长度
bool vis[maxn];
SAM(){ //link指向状态内最长字符串的最长的一个在另一个endpos类的后缀
tot=last=1;//sz endpos大小
}
void extend(int c){
int cur=++tot,p=last;last=cur;sz[cur]=1;
len[cur]=len[p]+1;
for(;p&&!ch[p][c];p=link[p])ch[p][c]=cur;
if(!p)link[cur]=1;
else{
int q=ch[p][c];
if(len[p]+1==len[q])link[cur]=q;
else{
int clone=++tot;//==len[p]+1的复制出来
memcpy(ch[clone],ch[q],sizeof(ch[q]));
len[clone]=len[p]+1;
link[clone]=link[q];link[q]=link[cur]=clone;
for(;p&&ch[p][c]==q;p=link[p])ch[p][c]=clone;
}
}
}
void dfs(int x){
for(auto&v:G[x]){
dfs(v);
sz[x]+=sz[v];
}
}
void calSZ(){
for(int i=2;i<=tot;++i)G[link[i]].pb(i);
dfs(1);
}
void solve(char *s){
ll ans=0;
int L=strlen(s),p=1,nowlen=0;
for(int i=0;i<L;++i){
int c=s[i]-'a';
if(ch[p][c])nowlen++,p=ch[p][c];
else{
while(p&&!ch[p][c])p=link[p];
if(!p)p=1,nowlen=0;
else{
nowlen=len[p]+1;
p=ch[p][c];
}
}
}
for(int i=0;i<L;++i){
if(nowlen==L){
if(!vis[p]){
ans+=sz[p],vis[p]=1;
num[++cnt]=p;
}
if(--nowlen==len[link[p]])p=link[p];
}
int c=s[i]-'a';
if(ch[p][c])nowlen++,p=ch[p][c];
else{
while(p&&!ch[p][c])p=link[p];
if(!p)p=1,nowlen=0;
else{
nowlen=len[p]+1;
p=ch[p][c];
}
}
}
cout<<ans<<"\n";
while(cnt){
vis[num[cnt--]]=0;
}
}
}sam;
char s[maxn];
int n;
int main(){
scanf("%s",s);
int len=strlen(s);
for(int i=0;i<len;++i)sam.extend(s[i]-'a');
sam.calSZ();
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%s",s);
sam.solve(s);
}
return 0;
}
4.P6640 BJOI2020封印
题意:给 s s s和 t t t两个字符串, q q q次询问,每次问 s [ l , r ] s[l,r] s[l,r]和 t t t的最长公共子串。
对于字符串固定区间相关子串询问时,可以考虑对每一个前缀处理
对 t t t串建 S A M SAM SAM,然后让 s s s串在上面匹配,预处理处每
个前缀能匹配的最大后缀 f [ i ] f[i] f[i]
对于询问 [ l , r ] [l,r] [l,r],答案就是 m a x i = l r m i n ( f [ i ] , i − l + 1 ) max_{i=l}^{r}min(f[i],i-l+1) maxi=lrmin(f[i],i−l+1),我们发现里面 i − l + 1 − f [ i ] i-l+1-f[i] i−l+1−f[i]是不上升的,所以可以通过二分找到划分点,变成区间 m a x max max问题,用 s t st st表预处理即可,时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
三.SAM与后缀树
反串sam的parent树就是后缀树
前缀(i,j)的最长公共后缀长度=parent树上LCA的len值
1.P4248 差异
题意:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LdxHJkjg-1648594114522)(C:\Users\98753\AppData\Roaming\Typora\typora-user-images\image-20210814100730900.png)]
思路:
建出后缀树,两两后缀的 l c p lcp lcp通过在后缀树上 d p dp dp即可完成(标记对应后缀点)
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
using ll=long long;
const int maxn=1e6+5;
struct SAM{
int len[maxn],link[maxn],ch[maxn][26],last,tot,sz[maxn];//len指状态内最长长度
vector<int>G[maxn];
ll ans;
SAM(){ //link指向状态内最长字符串的最长的一个在另一个endpos类的后缀
tot=last=1;//sz endpos大小
}
void extend(int c){
int cur=++tot,p=last;last=cur;sz[cur]=1;
len[cur]=len[p]+1;
for(;p&&!ch[p][c];p=link[p])ch[p][c]=cur;
if(!p)link[cur]=1;
else{
int q=ch[p][c];
if(len[p]+1==len[q])link[cur]=q;
else{
int clone=++tot;//==len[p]+1的复制出来
memcpy(ch[clone],ch[q],sizeof(ch[q]));
len[clone]=len[p]+1;
link[clone]=link[q];link[q]=link[cur]=clone;
for(;p&&ch[p][c]==q;p=link[p])ch[p][c]=clone;
}
}
}
void dfs(int x){
for(auto&v:G[x]){
dfs(v);
ans+=(ll)sz[x]*sz[v]*len[x];
sz[x]+=sz[v];
}
}
ll solve(){
for(int i=2;i<=tot;++i)G[link[i]].pb(i);
dfs(1);
return ans;
}
}sam;
char s[maxn];
int main(){
scanf("%s",s);
int n=strlen(s);
for(int i=n-1;i>=0;--i)sam.extend(s[i]-'a');
int p=1;
cout<<(ll)n*(n+1)*(n-1)/2-2*sam.solve()<<"\n";
return 0;
}
四.广义SAM
本质是识别多模式串所有子串的自动机,构造分为离线和在线两种,离线需要建出 t r i e trie trie,在线每次插入一个模式串前将las设为1即可
广义 S A M SAM SAM的用法和一半 s a m sam sam区别不大,而且性质也非常像,唯一要注意的是每个结点的 e n d p o s endpos endpos虽然大小相同但分属于不同模式串,可以在插入的时候标记 i d id id
1.SP8093 JZPGYZ
题意:
给定 n n n个模板串,以及 m m m个查询串
依次查询每一个查询串是多少个模板串的子串
思路:
多模式串匹配, a c ac ac自动机或者 广 义 后 缀 自 动 机 广义后缀自动机 广义后缀自动机都可以解决,建的时候每个模式串的前缀标记,查询串暴力匹配直到找到对应点,广义后缀自动机相当于询问被匹配的点的子树的不同颜色数,这是个典中典问题,离线树状数组即可解决, O ( n l o g n ) O(nlogn) O(nlogn)
#include<bits/stdc++.h>
#define eb emplace_back
#define lowb(i) (i&(-i))
using namespace std;
const int N=6e4+5;
const int maxn=2e5+5;
int pre[maxn],num,c[maxn],M,id[maxn];
vector<int>col[maxn],G[maxn];
void add(int x,int val){
for(int i=x;i<=M;i+=lowb(i))c[i]+=val;
}
int ask(int x){
int ans=0;
for(int i=x;i;i-=lowb(i))ans+=c[i];
return ans;
}
struct Q{
int l,r,id;
bool operator<(const Q&x)const{
return r<x.r;
}
}q[N];
struct GSAM{
int link[maxn],ch[maxn][26],tot,len[maxn],in[maxn],out[maxn],ti;
GSAM(){ tot=1;}
int extend(int c,int last){
if(ch[last][c]){
int p=last,q=ch[p][c];
if(len[p]+1==len[q])return q;
else{
int clone=++tot;
len[clone]=len[p]+1;
memcpy(ch[clone],ch[q],sizeof(ch[q]));
while(p&&ch[p][c]==q)ch[p][c]=clone,p=link[p];
link[clone]=link[q];link[q]=clone;
return clone;
}
}
int cur=++tot,p=last;
len[cur]=len[p]+1;
while(p&&!ch[p][c])ch[p][c]=cur,p=link[p];
if(!p)link[cur]=1;
else{
int q=ch[p][c];
if(len[q]==len[p]+1)link[cur]=q;
else{
int clone=++tot;
len[clone]=len[p]+1;
memcpy(ch[clone],ch[q],sizeof(ch[q]));
while(p&&ch[p][c]==q)ch[p][c]=clone,p=link[p];
link[clone]=link[q];link[q]=link[cur]=clone;
}
}
return cur;
}
void dfs(int x){
in[x]=++ti;id[ti]=x;
for(auto&v:G[x]){
dfs(v);
}
out[x]=ti;
}
void init(){
for(int i=2;i<=tot;++i)G[link[i]].eb(i);
dfs(1);
}
int query(char *s){
int L=strlen(s),p=1;
for(int i=0;i<L;++i){
int c=s[i]-'a';
p=ch[p][c];
if(!p)return 0;
}
return p;
}
}sam;
char s[maxn<<1];
int ans[60005];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n,m;
cin>>n>>m;
for(int i=1;i<=n;++i){
cin>>s;
int L=strlen(s),las=1;
for(int j=0;j<L;++j){
las=sam.extend(s[j]-'a',las);
col[las].eb(i);
}
}
sam.init();
for(int i=1;i<=m;++i){
cin>>s;
int pos=sam.query(s);
if(!pos)ans[i]=0;
else q[++num]={sam.in[pos],sam.out[pos],i};
}
sort(q+1,q+1+num);
M=sam.tot;
int p=0;
for(int i=1;i<=num;++i){
while(p<q[i].r){
p++;
int pos=id[p];
for(auto&u:col[pos]){
if(pre[u])add(pre[u],-1);
add(p,1);
pre[u]=p;
}
}
ans[q[i].id]=ask(q[i].r)-ask(q[i].l-1);
}
for(int i=1;i<=m;++i)cout<<ans[i]<<"\n";
return 0;
}
2.bzoj 3926 诸神眷顾的幻想乡
题意:给定一颗树问所有本质不同子串
思路:叶子不超过20个,所以可以直接$dfs$20次建立广义后缀自动机,然后按本质不同子串随便算一下就好了
#include<bits/stdc++.h>
#define eb emplace_back
using namespace std;
using ll=long long;
const int N=1e5+5;
const int maxn=2e5+5;
vector<int>G[N];
int col[N],deg[N];
struct GSAM{
int link[maxn*20],ch[maxn*20][10],tot,len[maxn*20];
GSAM(){ tot=1;}
int extend(int c,int last){
if(ch[last][c]){
int p=last,q=ch[p][c];
if(len[p]+1==len[q])return q;
else{
int clone=++tot;
len[clone]=len[p]+1;
memcpy(ch[clone],ch[q],sizeof(ch[q]));
while(p&&ch[p][c]==q)ch[p][c]=clone,p=link[p];
link[clone]=link[q];link[q]=clone;
return clone;
}
}
int cur=++tot,p=last;
len[cur]=len[p]+1;
while(p&&!ch[p][c])ch[p][c]=cur,p=link[p];
if(!p)link[cur]=1;
else{
int q=ch[p][c];
if(len[q]==len[p]+1)link[cur]=q;
else{
int clone=++tot;
len[clone]=len[p]+1;
memcpy(ch[clone],ch[q],sizeof(ch[q]));
while(p&&ch[p][c]==q)ch[p][c]=clone,p=link[p];
link[clone]=link[q];link[q]=link[cur]=clone;
}
}
return cur;
}
ll solve(){
ll ans=0;
for(int i=2;i<=tot;++i)ans+=len[i]-len[link[i]];
return ans;
}
void dfs(int x,int fa,int fap){
int xfa=extend(col[x],fap);
for(auto&v:G[x]){
if(v==fa)continue;
dfs(v,x,xfa);
}
}
}gsam;
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n,c;
cin>>n>>c;
for(int i=1;i<=n;++i)cin>>col[i];
int u,v;
for(int i=1;i<n;++i){
cin>>u>>v;
G[u].eb(v);G[v].eb(u);
deg[u]++;deg[v]++;
}
for(int i=1;i<=n;++i){
if(deg[i]==1)gsam.dfs(i,0,1);
}
cout<<gsam.solve();
return 0;
}
3.2021牛客暑期多校C
题意:
思路:转化一下发现就是求f(S,x,y),本质不同子串考虑SAM,再转化一下就是 f ( S , i , n ) f(S,i,n) f(S,i,n)所有本质不同子串个数,发现字符集只有10,所以我们倒序插入 t r i e trie trie的话,一旦发现更大的就回滚再插入,然后对 t r i e trie trie建立广义后缀自动机,最坏情况是 O ( 100 n ) O(100n) O(100n)
思路:
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int maxn=2e6+5;
struct Trie{
int cnt,tr[maxn][26],fa[maxn],ch[maxn];
Trie(){ cnt=1;}
void insert(char*s){
int p=1,L=strlen(s+1);
for(int i=L;i>=1;--i){
int c=s[i]-'a',num=0;
while(p!=1&&c>ch[p]){
p=fa[p];num++;
}
num++;
while(num--){
if(!tr[p][c])tr[p][c]=++cnt,fa[cnt]=p,ch[cnt]=c;
p=tr[p][c];
}
}
}
}trie;
struct GSAM{
int tot,pos[maxn],link[maxn],len[maxn],ch[maxn][10];
queue<int>q;
GSAM(){ tot=1;}
int insert(int c,int last){
int cur=++tot,p=last;
len[cur]=len[p]+1;
while(p&&!ch[p][c])ch[p][c]=cur,p=link[p];
if(!p)link[cur]=1;
else{
int q=ch[p][c];
if(len[p]+1==len[q])link[cur]=q;
else{
int clone=++tot;
memcpy(ch[clone],ch[q],sizeof(ch[q]));
len[clone]=len[p]+1;
while(p&&ch[p][c]==q)ch[p][c]=clone,p=link[p];
link[clone]=link[q];link[q]=link[cur]=clone;
}
}
return cur;
}
void build(){
for(int i=0;i<10;++i)
if(trie.tr[1][i])q.push(trie.tr[1][i]);
pos[1]=1;
while(!q.empty()){
int x=q.front();q.pop();
pos[x]=insert(trie.ch[x],pos[trie.fa[x]]);
for(int i=0;i<10;++i)
if(trie.tr[x][i])q.push(trie.tr[x][i]);
}
}
void solve(){
ll ans=0;
for(int i=2;i<=tot;++i)ans+=len[i]-len[link[i]];
cout<<ans;
}
}gsam;
char s[maxn];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>(s+1);
trie.insert(s);
gsam.build();
gsam.solve();
return 0;
}
五.SAM上线段树合并
1.cf1037H
题意 : 给一个文章串 S S S,给出若干文本串 T T T。
截取 S S S 的一个字串 S 2 = S [ l . . . r ] S2=S[l...r] S2=S[l...r], 求 S 2 S2 S2的子串中,严格大于 T T T的字典序最小的串,如果没有输出-1
思路:
假如没有限制,即 S 2 = S S2=S S2=S,显然有个贪心,然后让 T T T在 S S S上面匹配,假如匹配不上了,就取最小的大于它的字符放在末尾,否则我们只能退而求其次,退回上一次,看看是否能大于 T p − 1 T_{p-1} Tp−1,到源点都没有的话,就输出-1
现在考虑存在限制
说明每次转移 T + A T+A T+A转移到的点都必须是好点, p a r e n t parent parent树上用线段树合并维护 e n d p o s endpos endpos集合即可,只要查询 [ l + l e n − 1 , r ] [l+len-1,r] [l+len−1,r]是否有值即可,注意此时的线段树每次都得新建结点,类似可持久化线段树
#include<bits/stdc++.h>
#define lson lc[p],l,mid
#define rson rc[p],mid+1,r
#define ls lc[p]
#define rs rc[p]
#define fi first
#define se second
#define eb emplace_back
using namespace std;
using PI=pair<char,int>;
const int maxn=2e5+5;
const int N=maxn*32;
int q;
struct SAM{
int len[maxn],link[maxn],ch[maxn][26],tot,last;
int lc[N],rc[N],rt[maxn],cnt,sum[N],n,sta[maxn];
char s2[maxn];
vector<int>G[maxn];
void pushUp(int p){
sum[p]=sum[ls]+sum[rs];
}
void update(int&p,int l,int r,int x,int val){
if(!p)p=++cnt;
if(l==r){
sum[p]+=val;return;
}
int mid=l+r>>1;
if(x<=mid)update(lson,x,val);
else update(rson,x,val);
pushUp(p);
}
int merge(int p,int q,int l,int r){
if(!p||!q)return p+q;
int x=++cnt,mid=l+r>>1;
sum[x]=sum[p]+sum[q];
if(l==r)return x;
lc[x]=merge(lc[p],lc[q],l,mid);
rc[x]=merge(rc[p],rc[q],mid+1,r);
return x;
}
int query(int p,int l,int r,int L,int R){
if(!p)return 0;
if(L<=l&&r<=R)return sum[p];
int mid=l+r>>1,ans=0;
if(L<=mid)ans+=query(lson,L,R);
if(R>mid)ans+=query(rson,L,R);
return ans;
}
SAM(){ tot=last=1;}
void extend(int c){
int cur=++tot,p=last;last=cur;
len[cur]=len[p]+1;
update(rt[cur],1,n,len[cur],1);
for(;p&&!ch[p][c];p=link[p])ch[p][c]=cur;
if(!p)link[cur]=1;
else{
int q=ch[p][c];
if(len[p]+1==len[q])link[cur]=q;
else{
int clone=++tot;
len[clone]=len[p]+1;
memcpy(ch[clone],ch[q],sizeof(ch[q]));
link[clone]=link[q];link[q]=link[cur]=clone;
for(;p&&ch[p][c]==q;p=link[p])ch[p][c]=clone;
}
}
}
void dfs(int x){
for(auto&v:G[x]){
dfs(v);
rt[x]=merge(rt[x],rt[v],1,n);
}
}
void solve(){
for(int i=2;i<=tot;++i)G[link[i]].eb(i);
dfs(1);
int q,l,r;
cin>>q;
for(int i=1;i<=q;++i){
int nxt,j,pos=1;
cin>>l>>r>>(s2+1);
int L=strlen(s2+1);
for(j=1;;++j){
sta[j]=-1;
int c=max(s2[j]-'a'+1,0);
for(int k=c;k<26;++k){
nxt=ch[pos][k];
if(nxt&&query(rt[nxt],1,n,l+j-1,r)){
sta[j]=k;
break;
}
}
nxt=ch[pos][s2[j]-'a'];
if(!nxt||(j==L+1)||!query(rt[nxt],1,n,l+j-1,r))break;//找不到 不是好点 找完了再加一个
pos=nxt;
}
while(j&&sta[j]==-1)j--;
if(!j)puts("-1");
else{
for(int k=1;k<j;++k)putchar(s2[k]);
putchar(sta[j]+'a');
puts("");
}
}
}
}sam;
char s[maxn];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>(s+1);
int len=strlen(s+1);
sam.n=len;
for(int i=1;i<=len;++i)sam.extend(s[i]-'a');
sam.solve();
return 0;
}
cf666E
题意:给你一个串 S 以及一个字符串数组$ T[1…m] , q 次 询 问 每 次 问 S 的 子 串 ,q次询问每次问 S的子串 ,q次询问每次问S的子串S[pl,pr]$在T1…r中的哪个串出现的最多,如有多解输出最靠前的那一个。
思路:
挺好想的一道题,这种区间限制都可以往parents树上想,我们可以对 T T T建出广义后缀自动机,在每个串前缀出现的点打上出现串的标记,预处理出 S S S的每个前缀最大匹配长度及其位置,询问的时候倍增找到包含 S [ p l , p r ] S[pl,pr] S[pl,pr]子串位置的点,就变成区间数字典序最小众数问题, p a r e n t s parents parents树上线段树合并即可,注意需要先判 l i m i t limit limit是否合法
#include<bits/stdc++.h>
#define fi first
#define se second
#define lson lc[p],l,mid
#define rson rc[p],mid+1,r
#define ls lc[p]
#define rs rc[p]
#define eb emplace_back
using namespace std;
typedef pair<int,int>PI;
const int maxn=1.3e6+5;
const int N=maxn*16;
vector<int>G[maxn];
int rt[maxn],n;
char s[maxn],s2[maxn];
struct GSAM{
int link[maxn],ch[maxn][26],tot,len[maxn];
int mx[N],lc[N],rc[N],fa[maxn][22],cnt,flag[N],ps[maxn],limit[maxn];
struct Node{
int l,r,pl,id;
Node(int _l=0,int _r=0,int _pl=0,int _id=0):l(_l),r(_r),pl(_pl),id(_id){ }
};
vector<Node>Q[maxn];
vector<pair<int,int>>ans;
GSAM(){ tot=1;cnt=0;}
void pushUp(int p){
if(mx[ls]>=mx[rs])mx[p]=mx[ls],flag[p]=flag[ls];
else mx[p]=mx[rs],flag[p]=flag[rs];
}
PI ask(int p,int l,int r,int L,int R){
if(!p)return PI(100000,0);
if(L<=l&&r<=R)return PI(flag[p],mx[p]);
int mid=l+r>>1;
PI x1={0,0},x2={ 0,0};
if(L<=mid)x1=ask(lson,L,R);
if(R>mid)x2=ask(rson,L,R);
if(x1.se>=x2.se)return x1;
else return x2;
}
void update(int&p,int l,int r,int x){
if(!p)p=++cnt;
if(l==r){
mx[p]++;flag[p]=l;return;
}
int mid=l+r>>1;
if(x<=mid)update(lson,x);
else update(rson,x);
pushUp(p);
}
int extend(int c, int last) {
if (ch[last][c]) {
int p = last, q = ch[p][c];
if (len[p] + 1 == len[q]) return q;
int newq = ++tot ;
memcpy(ch[newq], ch[q], sizeof(ch[q]));
len[newq] = len[p] + 1;
link[newq] = link[q];
link[q] = newq;
for (; p && ch[p][c] == q; p = link[p]) ch[p][c] = newq;
return newq;
}
int p = last, now = ++ tot;
len[now] = len[p] + 1;
for (; p && ! ch[p][c]; p = link[p]) ch[p][c] = now;
if (! p) {
link[now] = 1;
return now;
}
int q = ch[p][c];
if (len[q] == len[p] + 1) {
link[now] = q;
return now;
}
int newq = ++ tot;
memcpy(ch[newq], ch[q], sizeof(ch[q]));
link[newq] = link[q], len[newq] = len[p] + 1;
link[q] = link[now] = newq;
for (; p && ch[p][c] == q; p = link[p]) ch[p][c] = newq;
return now;
}
int merge(int p,int q,int l,int r){
if(!p||!q)return p+q;
int root=++cnt,mid=l+r>>1;
if(l==r){
mx[root]=mx[p]+mx[q];
flag[root]=l;
return root;
}
lc[root]=merge(lc[p],lc[q],l,mid);
rc[root]=merge(rc[p],rc[q],mid+1,r);
pushUp(root);
return root;
}
void dfs(int x,int f){
for(auto&v:G[x]){
dfs(v,x);
rt[x]=merge(rt[x],rt[v],1,n);
}
}
void init(){
int L=strlen(s+1),p=1,nowlen=0;
for(int i=1;i<=L;++i){
int c=s[i]-'a';
if(ch[p][c]){
p=ch[p][c];nowlen++;
}else{
while(p&&!ch[p][c])p=link[p];
if(!p)p=1,nowlen=0;
else{
nowlen=len[p]+1;
p=ch[p][c];
}
}
ps[i]=p;limit[i]=nowlen;
}
for(int i=1;i<=tot;++i)fa[i][0]=link[i];
for(int j=1;j<=20;++j)
for(int i=1;i<=tot;++i)
fa[i][j]=fa[fa[i][j-1]][j-1];
for(int i=2;i<=tot;++i)G[link[i]].eb(i);
dfs(1,0);
}
void solve(){
int m,l,r,pl,pr;
cin>>m;
for(int i=1;i<=m;++i){
cin>>l>>r>>pl>>pr;
int nowpos=ps[pr];
if(limit[pr]<pr-pl+1){
cout<<l<<" "<<"0\n";
continue;
}
for(int j=20;j>=0;--j){
if(fa[nowpos][j]&&len[fa[nowpos][j]]>=pr-pl+1)
nowpos=fa[nowpos][j];
}
PI w=ask(rt[nowpos],1,n,l,r);
if(!w.se)cout<<l<<" "<<"0\n";
else cout<<w.fi<<" "<<w.se<<"\n";
}
}
}gsam;
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>(s+1);
cin>>n;
for(int i=1;i<=n;++i){
cin>>(s2+1);
int las=1,len=strlen(s2+1);
for(int j=1;j<=len;++j){
las=gsam.extend(s2[j]-'a',las);
gsam.update(rt[las],1,n,i);
}
}
gsam.init();
gsam.solve();
return 0;
}
SAM的性质以及应用
1. S A M SAM SAM是个 d a g dag dag
2.每个结点状态是某段前缀的连续后缀
3. p a r e n t parent parent树上,往前面加字符,到儿子,删字符到父亲
4.从起始节点沿着转移边走,每条路径都对应着一个子串,即将走过的边上的字符首尾相连得到的子串(显然多条路径会到达同一个节点上)。
5.可以通过对 l e n len len计数排序得到前缀树和dag的拓扑序
应用
查找某个子串位于哪个节点
给定一个字符串,每次查询某个子串在哪个节点
以 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AXqoKjxM-1648594114524)(https://www.zhihu.com/equation?tex=[l%2Cr])] 表示子串
对前缀 r r r直接倍增往上跳到len[]合适的地方
最长可重叠重复子串
找一个子串,使得至少出现两次,可以重叠,求最长子串长度
显然就是right集合大于等于2的那些节点的最大的len
最长不可重叠重复子串
找一个子串,使得至少出现两次,不可重叠,求最长子串长度
不光要使得right集合大于等于2,而且还需要考虑最靠右的那个位置和最靠左的那个位置之间的距离
if(sz[u] >= 2) ans = max(ans, min(len[u], r[u] - l[u]));//l和r在拓扑序上维护即可
最长可重叠k次重复子串
找一个子串,使得至少出现k次,可以重叠,求最长子串长度
显然就是right集合大于等于k的那些节点的最大的len
最长不可重叠k次重复子串
找一个子串,使得至少出现k次,不可重叠,求最长子串长度
由于right集合直接求的话时空复杂度会爆炸,不妨先二分一下答案
设当前二分最长子串长度为x,那么只需要将 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xWFGBR31-1648594114524)(https://www.zhihu.com/equation?tex=len[pre[u]]+\lt+x+\le+len[u]])] 的那些u找出来,并分别计算一下它们的right集合,然后把这些位置从小到大排序之后扫一遍并贪心的放入选入集合即可(即当前这个位置能放就放,放不了(即会与之前选择的子串重叠)就不放)
两个字符串的最长公共子串
给定两个字符串,求它们的最长公共子串有多长
对于其中一个字符串建立sam,然后拿另外一个在上面匹配,同时更新最长匹配长度
多个字符串的最长公共子串
- 对第一个串建 S A M SAM SAM, 然后剩下的串都在上面匹配,维护每个串在每个状态能匹配的最大值,注意得在Parents树上把对祖先的影响也算进去,最后取所有串在每个状态的最小值的最大值即可
字典序第k小子串
题意:
长度为 n n n的字符串,求第 k k k小子串是什么(分本质不同与本质相同
思路:
s
u
m
[
i
]
sum[i]
sum[i]表示经过从某字符出发 经过i结点的子串数量 ,通过拓扑序
d
p
dp
dp即可求得,
t
=
0
/
1
t=0/1
t=0/1只是决定了
s
u
m
[
i
]
sum[i]
sum[i]的初始值,以及每个点要减去的大小而已。
在 s a m sam sam上 d f s dfs dfs,从小到大枚举要转移的点,假如小于 k k k就不转移减掉即可。一旦当前 k < = s z [ x ] k<=sz[x] k<=sz[x]就表明找到了
补充:2021ccpc桂林, 2021icpc沈阳也有sam的题,都挺傻逼的,快退役就不更新了。