SAM 后缀自动机小结

后缀自动机

资料

  1. 陈立杰冬令营SAM讲稿
  2. 后缀自动机(SAM)学习笔记 ouuan
  3. OI wiki SAM
  4. 后缀自动机学习笔记 Menci

笔记

后缀自动机上每个点代表的是一些 $endpos$ 相同且长度连续的一些字符串,状态 $u$ 表示的字符串的长度范围为 $[min(u),max(v)]$

每个结点的 $par$ 是所有满足 $endpos[u] \in endpos[v]$ 的 $v$ 中且 $endpos[v]$ 大小最小的状态 $v$。

有关 $par$ 的一些性质:

  1. $min(u)=max(par[u])+1$

  2. $par[u]$ 所表示的字符串都是 $u$ 的后缀。

自动机接受的字符串为 $S$ 的所有子串。

表示字符串前缀 $Pre$ 的状态 $u$ 的 $max(u) = |Pre|$,从 $u$ 跳 $par$ 的话可以遍历 $Pre$ 的所有后缀。

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
namespace SAM
{
int cnt=0,root,last;
int len[MAXN*2],par[MAXN*2];
LL sum[MAXN*2];
map<char,int> nx[MAXN*2];
void newnode(int &x)
{
x=++cnt;
len[cnt]=par[cnt]=sum[cnt]=0;
nx[cnt].clear();
}
void init(){cnt=0,newnode(root),last=root;}
void extend_sam(char c)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;

int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
nx [n]=nx [q];
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
last=cur,sum[cur]=1;
}
void build_sam(char s[])
{
int n=strlen(s+1);
for(int i=1;i<=n;i++)
extend_sam(s[i]);//,debug();
}
};

技巧 & 应用

求 $endpos$ 集合的大小/最大值/最小值

考虑 $par$ 树的所有节点,$endpos$ 中 含有 $i$ 的深度最大的节点一定是表示前缀 $Pre_{i}$ 的状态,标记这个节点后用 dfs/拓扑/基排 逆推即可。

如果要求每个点 $endpos$ 的具体值可能需要 线段树合并/平衡树启发式合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int seq[MAXN*2],buc[N];
void sort_by_len()
{
fp(i,1,cnt) buc[len[i]]++;
fp(i,1,N-1) buc[i]+=buc[i-1];
fp(i,1,cnt) seq[buc[len[i]]--]=i;
}
void solve()
{
fp(i,1,cnt)
{
int cur=seq[i];
f[par[cur]]+=f[cur];
//f[par[cur]]=min(f[par[cur]],f[cur]);
//f[par[cur]]=max(f[par[cur]],f[cur]);
}
}

$logn$ 查找某个子串在 $SAM$ 上对应的状态

考虑倍增,预处理出每个节点在 $par$ 树上的$2^j$ 祖先,从表示 $S[1…r]$ 的叶子往上跳找到深度最小且满足 $max(u)>=(r-l+1)$的 $u$ 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void init()
{
for(int i=1;i<=cnt;i++) nx[i][0]=par[i];
fp(k,1,20)fp(i,1,cnt)nx[i][k]=nx[nx[i][k-1]][k-1];
}
LL count(int l,int r)
{
int cur=suf[r];
fd(k,20,0)
if(nx[cur][k]&&len[nx[cur][k]]>=r-l+1)
cur=nx[cur][k];
return sum[cur];
}

两个串的LCS

对 $S$ 串建立 $SAM$,拿 $T$ 串到 $SAM$ 上“运行”

具体来讲就是考虑 $T$ 的每个前缀 $pre_{i}$ 有多长的后缀是 $S$ 的一个子串,从 $pre_{i}$ 转移到 $pre_{i+1}$ 时看下当前状态是否存在到 $T[i+1]$ 的转移边,若不存在就不断跳 $par$ (删掉前面的一部分) 直到找到存在转移边的状态,同时维护在每个状态下的匹配长度,最后取个 $max$ 就是答案。

匹配过程十分类似于 $kmp$ 和 $AC$自动机 啊,,,

1
2
3
4
5
6
7
8
9
10
11
12
13
void solve(char s[])
{
int L=0,ans=0,cur=root,n=strlen(s+1);
for(int i=1;i<=n;i++)
{
int c=s[i]-'a';
while(cur!=root&&!nx[cur][c])
cur=par[cur],L=len[cur];
if(nx[cur][c]) cur=nx[cur][c],L++;
ans=max(ans,L);
}
printf("%d\n",ans);
}

本质不同的子串个数

相同的子串一定被同一个状态所表示,所以对后缀自动机上每个状态所表示的子串长度求和即是答案。

最小表示法

先将字符串 $S$ 复制一倍后得到 $T$,可以发现 $T$ 的长度为 $n$ 的子串包含了 $S$ 通过循环移位所能得到的所有字符串,所以问题转化为求 $T$ 的长度为 $n$ 的子串中字典序最小的一个。

对 $T$ 建立 $SAM$ 后从 $root$ 开始贪心走转移边中字符最小的那条即可。

字典序k大子串

统计出在每个状态通过转移边能到达的状态的数量 $f[u]$ ,从根节点开始从小到大枚举转移边 $v$ 若 $f[v]>=k$ 则从 $u$ 转移到 $v$,否则将 $k$ 减去 $f[v]$ 后继续尝试字符更大的转移边。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void print(int cur,int k,int t,LL f[])
{
if(!k) return;
for(auto trans:nx[cur])
{
if(f[trans.se]>=k)
{
putchar(trans.fi),print(trans.se,k-(t?sum[trans.se]:1),t,f);
break;
}
else k-=f[trans.se];
}
}
void solve(int k,int t)
{
fp(i,1,cnt) buc[len[i]]++;
fp(i,1,N-1) buc[i]+=buc[i-1];
fp(i,1,cnt) seq[buc[len[i]]--]=i;
fd(i,cnt,1) sum[par[seq[i]]]+=sum[seq[i]];

fd(i,cnt,1)
{
int cur=seq[i];
f[0][cur]=1,f[1][cur]=sum[cur];
for(auto trans:nx[cur])
{
f[0][cur]+=f[0][trans.se];
f[1][cur]+=f[1][trans.se];
}
}

if(f[t][root]-(t==1?sum[root]:1)<k) printf("-1\n");
else print(root,k,t,f[t]),putchar('\n');
}

多个串的LCS

对其中一个串建立 $SAM$ 把剩下所有串放到 $SAM$ 上运行,类似于求两个串的 $LCS$ 的过程,运行每一个串的时候记录它在当前状态上的最大匹配长度,注意不同串在同一个状态上的匹配长度要取 $min$,最后统计答案即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void run(string &s,int id)
{
mst(f,0);
int cur=root,L=0;
fp(i,0,s.size()-1)
{
int c=s[i]-'a';
while(cur!=root&&!nx[cur][c])
cur=par[cur],L=len[cur];
if(nx[cur][c]) cur=nx[cur][c],L++;
f[cur]=max(f[cur],L);
}
fd(i,cnt,1)
{
int cur=seq[i];
if(f[cur]) f[par[cur]]=len[par[cur]];
}
if(id==2) fp(i,1,cnt) ans[i]=f[i];
else fp(i,1,cnt) ans[i]=min(ans[i],f[i]);
}
void solve()
{
int L=0;
fp(i,1,cnt) L=max(ans[i],L);
cout << L << endl;
}

广义SAM

将 $SAM$ 推广到字典树上的产物,目前不太理解,,,

定义 $Trie$ 树的一个子串为节点 $u$ 到它子树中的节点 $v$ 的路径所形成的字符串,那么广义 $SAM$ 接受的状态为 $Tire$ 树上的所有子串。

$endpos$ 可视为 $Trie$ 树上的节点,$par$ 的定义不变。

模板

离线构造,需要预先知道 $Tire$ 树的形态,从根节点开始 $bfs$,建立新节点时拿他父亲的状态作为 $last$ 执行普通 $SAM$ 的 $extend$ 即可。

还有一种做法是每插入一个串就把 $last$ 指向 $root$,但这样可能建出一些无用的节点(考虑一个字符串完全包含另一个的情况)。

[ZJOI2015]诸神眷顾的幻想乡 求多个串本质不同的子串个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
const int K = 10;

namespace SAM
{
int root,cnt;
int len[MAXN*2],par[MAXN*2];
int nx[MAXN*2][K+1];
void newnode(int &x){x=++cnt;}
void init(){cnt=0,newnode(root);}
int extend(int c,int last)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;
int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
memcpy(nx[n],nx[q],sizeof(nx[q]));
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
return cur;
}
void solve()
{
LL ans=0;
fp(i,1,cnt) ans+=len[i]-len[par[i]];
io.write(ans);
io.push('\n');
}
}

namespace Trie
{
int root,cur,cnt;
int nx[MAXN][K+1],fa[MAXN],last[MAXN];
void newnode(int &x){x=++cnt;}
void init(){cnt=0,newnode(root),cur=root;}
void insert(int c)
{
if(!nx[cur][c])
newnode(nx[cur][c]),fa[nx[cur][c]]=cur;
cur=nx[cur][c];
}
void back(){cur=fa[cur];}
void build_sam()
{
SAM::init();
queue<int> q;
q.push(root);
last[root]=SAM::root;
while(!q.empty())
{
int cur=q.front(); q.pop();
fp(i,0,K)if(nx[cur][i])
last[nx[cur][i]]=SAM::extend(i,last[cur]),
q.push(nx[cur][i]);
}
}
};

struct edge{int u,v,next;};
vector<edge> e;
int n,c;
int pre[MAXN],col[MAXN],in[MAXN];
void addedge(int u,int v){e.pb({u,v,pre[u]}),pre[u]=e.size()-1;}
void dfs(int u,int fa)
{
Trie::insert(col[u]);
go(u) if(v!=fa) dfs(v,u);
Trie::back();
}

int work()
{
io.read(n),io.read(c),e.pb(edge());
fp(i,1,n) io.read(col[i]);
fp(i,1,n-1)
{
static int u,v;
io.read(u),io.read(v);
addedge(u,v),addedge(v,u);
in[u]++,in[v]++;
}
Trie::init();
fp(i,1,n) if(in[i]==1) dfs(i,-1);
Trie::build_sam();
SAM::solve();
return 0;
}

练习

Luogu P3804 【模板】后缀自动机

统计每个状态 $u$ 的 $endpos$ 集合的大小 $siz[u]$,所有的 $siz[u]*len[u]$ 取个 $max$ 就是答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
namespace SAM
{
int cnt=0,root,last;
int len[MAXN*2],par[MAXN*2];
LL sum[MAXN*2];
map<char,int> nx[MAXN*2];
void debug()
{
cout << dbgs2(root,last) << endl;
for(int i=1;i<=cnt;i++)
{
cout << dbgs3(i,len[i],par[i]) << endl;
for(auto t:nx[i])
cout << dbgs2(t.fi,t.se) << endl;
}
cout << endl;
}
void newnode(int &x)
{
x=++cnt;
len[cnt]=par[cnt]=sum[cnt]=0;
nx[cnt].clear();
}
void init(){cnt=0,newnode(root),last=root;}
void extend_sam(char c)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;

int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
nx [n]=nx [q];
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
last=cur,sum[cur]=1;
}
void build_sam(char s[])
{
int n=strlen(s+1);
for(int i=1;i<=n;i++)
extend_sam(s[i]);//,debug();
}
};

char s[MAXN];
struct edge{int u,v,next;};
vector<edge> e={edge()};
int pre[MAXN*2];
void addedge(int u,int v){e.pb({u,v,pre[u]}),pre[u]=e.size()-1;}
LL ans=0;
void dfs(int u)
{
go(u) dfs(v),SAM::sum[u]+=SAM::sum[v];
//cout << dbgs3(u,SAM::len[u],SAM::sum[u]) << endl;
if(SAM::sum[u]>1) ans=max(ans,SAM::len[u]*SAM::sum[u]);
}
void solve()
{
fp(i,2,SAM::cnt)
addedge(SAM::par[i],i);
dfs(1);
}

int work()
{
scanf("%s",s+1);

SAM::init();
SAM::build_sam(s);
solve();

printf("%lld\n",ans);
return 0;
}

Luogu P1368 工艺

求字符串的最小表示法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
namespace SAM
{
const int K = 26;

int cnt=0,root,last;
int len[MAXN*2],par[MAXN*2];
map<int,int> nx[MAXN*2];

void newnode(int &x)
{
x=++cnt;
len[cnt]=par[cnt]=0;
nx[cnt].clear();
}
void init(){cnt=0,newnode(root),last=root;}
void extend_sam(int c)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;

int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
nx [n]=nx [q];
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
last=cur;
}
void build_sam(int s[],int n){fp(i,1,n) extend_sam(s[i]);}
void solve(int n)
{
int cur=root;
while(n--)
{
io.write((nx[cur].begin())->fi);
io.push(' ');
cur=(nx[cur].begin())->se;
}
io.push('\n');
}
}

int n;
int s[MAXN];

int work()
{
io.read(n);
fp(i,1,n) io.read(s[i]),s[i+n]=s[i];
n*=2;

SAM::init();
SAM::build_sam(s,n);
SAM::solve(n/2);

return 0;
}

luogu P3804 【模板】后缀自动机

统计 $endpos$ 集合的大小,找到最大的 $sum[i]*len[i]$ 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
namespace SAM
{
int cnt=0,root,last;
int len[MAXN*2],par[MAXN*2];
LL sum[MAXN*2];
map<char,int> nx[MAXN*2];
void debug()
{
cout << dbgs2(root,last) << endl;
for(int i=1;i<=cnt;i++)
{
cout << dbgs3(i,len[i],par[i]) << endl;
for(auto t:nx[i])
cout << dbgs2(t.fi,t.se) << endl;
}
cout << endl;
}
void newnode(int &x)
{
x=++cnt;
len[cnt]=par[cnt]=sum[cnt]=0;
nx[cnt].clear();
}
void init(){cnt=0,newnode(root),last=root;}
void extend_sam(char c)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;

int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
nx [n]=nx [q];
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
last=cur,sum[cur]=1;
}
void build_sam(char s[])
{
int n=strlen(s+1);
for(int i=1;i<=n;i++)
extend_sam(s[i]);//,debug();
}
};

char s[MAXN];
struct edge{int u,v,next;};
vector<edge> e={edge()};
int pre[MAXN*2];
void addedge(int u,int v){e.pb({u,v,pre[u]}),pre[u]=e.size()-1;}
LL ans=0;
void dfs(int u)
{
go(u) dfs(v),SAM::sum[u]+=SAM::sum[v];
//cout << dbgs3(u,SAM::len[u],SAM::sum[u]) << endl;
if(SAM::sum[u]>1) ans=max(ans,SAM::len[u]*SAM::sum[u]);
}
void solve()
{
fp(i,2,SAM::cnt)
addedge(SAM::par[i],i);
dfs(1);
}

int work()
{
scanf("%s",s+1);

SAM::init();
SAM::build_sam(s);
solve();

printf("%lld\n",ans);
return 0;
}

SPOJ 1811 LCS

两个串的 $LCS$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
namespace SAM
{
int cnt=0,root,last;
int len[MAXN*2],par[MAXN*2];
int nx[MAXN*2][K];
void newnode(int &x)
{
x=++cnt;
len[cnt]=par[cnt]=0;
for(int i=0;i<K;i++)
nx[cnt][i]=0;
}
void init(){cnt=0,newnode(root),last=root;}
void extend_sam(int c)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;

int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
for(int i=0;i<K;i++)
nx[n][i]=nx[q][i];
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
last=cur;
}
void build_sam(char s[])
{
int n=strlen(s+1);
for(int i=1;i<=n;i++)
extend_sam(s[i]-'a');
}
void solve(char s[])
{
int L=0,ans=0,cur=root,n=strlen(s+1);
for(int i=1;i<=n;i++)
{
int c=s[i]-'a';
while(cur!=root&&!nx[cur][c])
cur=par[cur],L=len[cur];
if(nx[cur][c]) cur=nx[cur][c],L++;
ans=max(ans,L);
}
printf("%d\n",ans);
}
};

char a[MAXN],b[MAXN];

int work()
{
scanf("%s",a+1);
scanf("%s",b+1);

SAM::init();
SAM::build_sam(a);
SAM::solve(b);

return 0;
}

Luogu P3649 [APIO2014]回文串

马拉车 + SAM上倍增 其实是PAM模板题

首先考虑如何用马拉车求出本质不同的回文串,可以发现当扩展最长回文串的右边界时才会产生新的回文串,用 $SAM$ 统计出现次数即可。

注意扩展右边界时得到的回文串不一定就在之前从未出现过,如 $abadeaba$,所以用马拉车求本质不同回文串需要配上 $hash$ 来去重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
namespace SAM
{
const int K = 26;

int cnt=0,root,last;
int len[MAXN*2],par[MAXN*2],suf[MAXN];
LL sum[MAXN*2];
int nx[MAXN*2][K];
//int f [MAXN*2][21];

struct edge{int u,v,next;};
vector<edge> e={edge()};
int pre[MAXN*2];
void addedge(int u,int v){e.pb({u,v,pre[u]}),pre[u]=e.size()-1;}
void dfs(int u){go(u)dfs(v),sum[u]+=sum[v];}
void build_graph(){fp(i,2,cnt) addedge(par[i],i);}

void newnode(int &x)
{
x=++cnt;
len[cnt]=par[cnt]=sum[cnt]=0;
fp(i,0,K-1) nx[cnt][i]=0;
}
void init(){cnt=0,newnode(root),last=root;}
void extend_sam(int i,int c)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;

int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
fp(i,0,K-1) nx[n][i]=nx[q][i];
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
last=cur,sum[cur]=1,suf[i]=cur;
}
void build_sam(char s[])
{
int n=strlen(s+1);
fp(i,1,n) extend_sam(i,s[i]-'a');
build_graph(),dfs(root);

for(int i=1;i<=cnt;i++) nx[i][0]=par[i];
fp(k,1,20)fp(i,1,cnt)nx[i][k]=nx[nx[i][k-1]][k-1];
}
void debug()
{
for(int i=1;i<=cnt;i++)
{
cout << dbgs4(i,len[i],par[i],sum[i]) << endl;
}
}
LL count(int l,int r)
{
int cur=suf[r];
fd(k,20,0)
if(nx[cur][k]&&len[nx[cur][k]]>=r-l+1)
cur=nx[cur][k];
return sum[cur];
}
}

LL ans=0;
void calc(int l,int r){ans=max(ans,LL(r-l+1)*SAM::count(l,r));}

namespace manacher
{
int p[MAXN*2];
string a;
void run(char s[])
{
int m=strlen(s+1);
a.pb('$'),a.pb('#');
for(int i=1;i<=m;i++)
a.pb(s[i]),a.pb('#');
a.pb('\0');
int n=a.size()-1,id=0,mx=0;
for(int i=1;i<=n;i++)
{
if(mx>i) p[i]=min(p[2*id-i],mx-i+1);
else p[i]=1;
while(a[i+p[i]]==a[i-p[i]])
{
if(a[i+p[i]]!='#')
calc((i-p[i])/2,(i+p[i])/2);
p[i]++;
}
if(i+p[i]-1>mx) mx=i+p[i]-1,id=i;
}
}
}

int n;
char s[MAXN];

int work()
{
scanf("%s",s+1);
n=strlen(s+1);

SAM::init();
SAM::build_sam(s);
//SAM::debug();

manacher::run(s);
for(int i=1;i<=n;i++) calc(i,i);

return printf("%lld\n",ans);
}

Luogu P4070 [SDOI2016]生成魔咒

题意:求给定字符串每个前缀的本质不同子串个数

扩展 $SAM$ 时加上新节点的贡献即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
namespace SAM
{
const int K = 26;

int cnt=0,root,last;
int len[MAXN*2],par[MAXN*2];
LL sum[MAXN*2],ans=0;
map<int,int> nx[MAXN*2];

struct edge{int u,v,next;};
vector<edge> e={edge()};
int pre[MAXN*2];
void addedge(int u,int v){e.pb({u,v,pre[u]}),pre[u]=e.size()-1;}
void dfs(int u){go(u) dfs(v),sum[u]+=sum[v];}

void newnode(int &x)
{
x=++cnt;
len[cnt]=par[cnt]=0;
nx[cnt].clear();
}
void init(){cnt=0,newnode(root),last=root;}
void extend_sam(int c)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;

int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
nx [n]=nx [q];
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
last=cur,sum[cur]=1;
ans+=len[last]-len[par[last]];
io.write(ans);
io.push('\n');
}
void build_sam(int s[],int n)
{
fp(i,1,n) extend_sam(s[i]);
}
}

int n;
int a[MAXN];

int work()
{
io.read(n);
fp(i,1,n) io.read(a[i]);

SAM::init();
SAM::build_sam(a,n);

return 0;
}

Luogu P3975 [TJOI2015]弦论

K小子串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
namespace SAM
{
int cnt=0,root,last;
int len[MAXN*2],par[MAXN*2];
LL sum[MAXN*2],f[2][MAXN*2];
map<char,int> nx[MAXN*2];
void newnode(int &x)
{
x=++cnt;
len[x]=par[x]=sum[x]=f[0][x]=f[1][x]=0;
nx[x].clear();
}
void init(){cnt=0,newnode(root),last=root;}
void extend_sam(char c)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;

int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
nx [n]=nx [q];
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
last=cur,sum[cur]=1;
}
void build_sam(char s[])
{
int n=strlen(s+1);
fp(i,1,n) extend_sam(s[i]);
}
int seq[MAXN*2],buc[N];
void print(int cur,int k,int t,LL f[])
{
if(!k) return;
//cout << dbgs2(cur,k) << endl;
for(auto trans:nx[cur])
{
if(f[trans.se]>=k)
{
putchar(trans.fi),print(trans.se,k-(t?sum[trans.se]:1),t,f);
break;
}
else k-=f[trans.se];
}
}
void solve(int k,int t)
{
fp(i,1,cnt) buc[len[i]]++;
fp(i,1,N-1) buc[i]+=buc[i-1];
fp(i,1,cnt) seq[buc[len[i]]--]=i;

//fp(i,1,cnt) cout << dbgs3(seq[i],len[seq[i]],par[seq[i]]) << endl;

fd(i,cnt,1) sum[par[seq[i]]]+=sum[seq[i]];

fd(i,cnt,1)
{
int cur=seq[i];
//cout << dbgs2(cur,sum[cur]) << endl;
f[0][cur]=1,f[1][cur]=sum[cur];
for(auto trans:nx[cur])
{
f[0][cur]+=f[0][trans.se];
f[1][cur]+=f[1][trans.se];
}
//cout << dbgs3(cur,f[0][cur],f[1][cur]) << endl;
}

if(f[t][root]-(t==1?sum[root]:1)<k) printf("-1\n");
else print(root,k,t,f[t]),putchar('\n');
}
}

int n,t,k;
char s[MAXN];

int work()
{
scanf("%s",s+1);
n=strlen(s+1);

scanf("%d%d",&t,&k);

SAM::init();
SAM::build_sam(s);
SAM::solve(k,t);

return 0;
}

Luogu P2463 [SDOI2008]Sandy的卡片

多个串LCS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
namespace SAM
{
int cnt=0,root,last;
int len[MAXN*2],par[MAXN*2];
int f[MAXN*2],ans[MAXN*2];
map<int,int> nx[MAXN*2];

void newnode(int &x){x=++cnt;}
void init(){cnt=0,newnode(root),last=root;}
void extend_sam(int c)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;

int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
nx [n]=nx [q];
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
last=cur;
}
int seq[MAXN*2],buc[N];
void sort_by_len()
{
fp(i,1,cnt) buc[len[i]]++;
fp(i,1,N-1) buc[i]+=buc[i-1];
fp(i,1,cnt) seq[buc[len[i]]--]=i;
}
void build_sam(vector<int> &s)
{
int n=s.size();
fp(i,0,n-1) extend_sam(s[i]);
sort_by_len();
}
void run(vector<int> &s,int id)
{
mst(f,0);
int cur=root,L=0;
fp(i,0,s.size()-1)
{
int c=s[i];
while(cur!=root&&!nx[cur][c])
cur=par[cur],L=len[cur];
if(nx[cur][c]) cur=nx[cur][c],L++;
f[cur]=max(f[cur],L);
}
fd(i,cnt,1)
{
int cur=seq[i];
if(f[cur]) f[par[cur]]=len[par[cur]];
}
if(id==2) fp(i,1,cnt) ans[i]=f[i];
else fp(i,1,cnt) ans[i]=min(ans[i],f[i]);
}
void solve()
{
int L=0;
fp(i,1,cnt) if(ans[i]!=inf) L=max(ans[i],L);
io.write(L+1);
}
}

int n,m;
vector<int> s[MAXN];

int work()
{
io.read(m);
fp(i,1,m)
{
static int tmp;
io.read(n);
fp(j,1 ,n) io.read(tmp),s[i].pb(tmp);
fd(j,n-1,1) s[i][j]-=s[i][j-1];
s[i].erase(s[i].begin());
//cout << dbgs(i) << endl;
}

SAM::init();
SAM::build_sam(s[1]);
//cout << "done" << endl;

fp(i,2,m) SAM::run(s[i],i);
SAM::solve();

return 0;
}

BZOJ 1396 识别子串

设每个位置 $i$ 的最短识别子串长度为 $len[i]$

考虑那些 $endpos$ 集合大小为 $1$ 的叶子节点 $u$,设它唯一的 $endpos$ 为 $i$,则这个节点对每个位置的贡献可分两种情况讨论:

满足 $j\in[i-min(u) + 1,i]$ 的 $len[j]$ 和 $min(u)$ 取 $min$

满足 $j\in[1,i-min(i)]$ 的 $len[j]$ 和 $i-j+1$ 取 $min$

考虑两种情况分别用线段树维护,那么第一种状态的更新相当于区间每个数对 $min(u)$ 取 $min$,第二种更新 $i-j+1$ 中的 $j$ 可以提出来,同样的做法维护最小的 $i+1$ 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
struct sgt
{
int mn[MAXN*4],v[MAXN*4];
void doMin(int cnt,int &val)
{
mn[cnt]=min(mn[cnt],val);
v [cnt]=min(v [cnt],val);
}
void pushdown(int cnt)
{
if(mn[cnt]!=inf)
{
doMin(ls,mn[cnt]);
doMin(rs,mn[cnt]);
mn[cnt]=inf;
}
}
void build(int l,int r,int cnt)
{
mn[cnt]=inf,v[cnt]=inf;
if(l==r) return;
build(l,mid,ls);
build(mid+1,r,rs);
}
void update(int l,int r,int nl,int nr,int v,int cnt)
{
if(l==nl&&r==nr){doMin(cnt,v);return;}
pushdown(cnt);
if(nr<=mid) update(l,mid,nl,nr,v,ls);
else if(nl>mid) update(mid+1,r,nl,nr,v,rs);
else update(l,mid,nl,mid,v,ls),update(mid+1,r,mid+1,nr,v,rs);
}
void get_val(int a[],int l,int r,int cnt)
{
if(l==r){a[l]=v[cnt];return;}
pushdown(cnt);
get_val(a,l,mid,ls);
get_val(a,mid+1,r,rs);
}
}T1,T2;

const int K = 26;

namespace SAM
{
int cnt=0,root,last;
int len[MAXN*2],par[MAXN*2],s[MAXN*2];
int nx[MAXN*2][K],pre[MAXN];

void newnode(int &x){x=++cnt;}
void init(){cnt=0,newnode(root),last=root;}
void extend_sam(int c,int index)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;

int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
for(int i=0;i<K;i++)
nx[n][i]=nx[q][i];
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
last=cur,pre[index]=cur,s[cur]=1;
}
int seq[MAXN*2],buc[N];
void sort_by_len()
{
fp(i,1,cnt) buc[len[i]]++;
fp(i,1,N-1) buc[i]+=buc[i-1];
fp(i,1,cnt) seq[buc[len[i]]--]=i;
}
void build_sam(char s[])
{
int n=strlen(s+1);
fp(i,1,n) extend_sam(s[i]-'a',i);
}
void solve(int n)
{
sort_by_len();
fd(i,cnt,1)
{
int cur=seq[i];
s[par[cur]]+=s[cur];
}
fp(i,1,n)
{
int cur=pre[i];
if(s[cur]==1)
{
int l=len[par[cur]]+1,r=len[cur];
T1.update(1,n,i-r+1,i-l+1,i,1);
if(i-l+2<=i) T2.update(1,n,i-l+2,i,l,1);
}
}
}
}

int n;
char s[MAXN];
int v1[MAXN],v2[MAXN];

int work()
{
scanf("%s",s+1);
n=strlen(s+1);

T1.build(1,n,1);
T2.build(1,n,1);

SAM::init();
SAM::build_sam(s);
SAM::solve(n);


T1.get_val(v1,1,n,1);
T2.get_val(v2,1,n,1);

fp(i,1,n) printf("%d\n",min(v2[i],v1[i]-i+1));

return 0;
}

Luogu P3346 [ZJOI2015]诸神眷顾的幻想乡

由于叶子最多只有 $20$ 个,我们可以把每个叶子当作根跑出一颗 $Trie$ 出来,那么 20 个 $Trie$ 合并起来就包含了所有原树上任意两个路径所构成的字符串

然后建立广义 $SAM$,最后在求一波本质不同子串即可

代码见上面

BZOJ 3277 串

贴个别人的题解,,,

一个用SAM维护多个串的根号特技

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
int n,k;
string s[MAXN];

const int K = 26;

namespace SAM
{
int cnt=0,root,last;
int len[MAXN*2],par[MAXN*2],sum[MAXN*2];
map<char,int> nx[MAXN*2];
int vis[MAXN*2];
LL f[MAXN*2];

void newnode(int &x){x=++cnt;}
void init(){cnt=0,newnode(root),last=root;}
void extend_sam(char c,int index)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;

int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
nx[n]=nx[q];
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
last=cur;
}
int seq[MAXN*2],buc[N];
void sort_by_len()
{
fp(i,1,cnt) buc[len[i]]++;
fp(i,1,N-1) buc[i]+=buc[i-1];
fd(i,cnt,1) seq[buc[len[i]]--]=i;
}
void build_sam(string &s)
{
int n=s.size();
fp(i,0,n-1) extend_sam(s[i],i);
last=root;
}
void mark(int cur,int id)
{
while(cur!=root&&vis[cur]!=id)
sum[cur]++,vis[cur]=id,cur=par[cur];
}
void solve()
{
sort_by_len();
fp(i,1,cnt) if(sum[i]>=k) f[i]+=len[i]-len[par[i]];
fp(i,1,cnt)
{
int cur=seq[i];
f[cur]+=f[par[cur]];
}
}
void run(string &s,int id,bool flag=0)
{
int cur=root,n=s.size();
LL ans=0;
fp(i,0,n-1)
{
cur=nx[cur][s[i]];
if(!flag) mark(cur,id);
else ans+=f[cur];
}
if(flag) cout << ans << " ";
}
}


int work()
{
cin >> n >> k;

SAM::init();
fp(i,1,n) cin >> s[i],SAM::build_sam(s[i]);
fp(i,1,n) SAM::run(s[i],i,0);

SAM::solve();
fp(i,1,n) SAM::run(s[i],i,1);

return 0;
}

CF235C Cyclical Quest

题意:
当两个字符串可以通过循环移位变成完全一样时我们称这两个字符串“相等”。

给定文本串,多个模式串,询问文本串中与模式串“
相等”的子串有多少

SOL:先对文本串建立 $SAM$,考虑计算模式串的所有循环移位在文本串中的出现次数

把模式串拿到 $SAM$ 上运行并记录在每个状态的匹配长度 L,每次去掉首字母并把他加到字符串末尾,若 $L-1<min(u)$ 或没有转移边就暴力跳 $par$即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
namespace SAM
{
const int K = 26;

int cnt=0,root,last;
int len[MAXN*2],par[MAXN*2];
LL sum[MAXN*2];
bool calc[MAXN*2];
map<int,int> nx[MAXN*2];

struct edge{int u,v,next;};
vector<edge> e={edge()};
int pre[MAXN*2];
void addedge(int u,int v){e.pb({u,v,pre[u]}),pre[u]=e.size()-1;}
void dfs(int u){go(u) dfs(v),sum[u]+=sum[v];}

void newnode(int &x)
{
x=++cnt;
len[cnt]=par[cnt]=0;
nx[cnt].clear();
}
void init(){cnt=0,newnode(root),last=root;}
void extend_sam(int c)
{
int cur;
newnode(cur);
len[cur]=len[last]+1;

int p=last;
while(p&&!nx[p][c])
nx[p][c]=cur,p=par[p];
if(!p) par[cur]=root;
else
{
int q=nx[p][c];
if(len[q]==len[p]+1) par[cur]=q;
else
{
int n;
newnode(n);
len[n]=len[p]+1;
par[n]=par[q];
nx [n]=nx [q];
while(p&&nx[p][c]==q)
nx[p][c]=n,p=par[p];
par[q]=par[cur]=n;
}
}
last=cur,sum[cur]=1;
}
void build_sam(char s[])
{
int n=strlen(s+1);
fp(i,1,n) extend_sam(s[i]-'a');
fp(i,2,cnt) addedge(par[i],i);
dfs(root);

//fp(i,2,cnt) cout << dbgs2(i,sum[i]) << endl;
}
stack<int> stk;
void match(char s[],LL ans=0)
{
while(!stk.empty())
calc[stk.top()]=0,stk.pop();

int n=strlen(s+1);

int cur=root,l=0;
fp(i,1,n)
{
int c=s[i]-'a';
while(cur!=root&&!nx[cur][c]) cur=par[cur],l=len[cur];
if(nx[cur][c]) cur=nx[cur][c],l++;
}

fp(i,1,n)
{
//cout << dbgs2(i,cur) << endl;

if(l==n) l--;
if(cur!=root&&len[par[cur]]>=l) cur=par[cur];

int c=s[i]-'a';
while(cur!=root&&!nx[cur][c]) cur=par[cur],l=len[cur];
if(nx[cur][c]) cur=nx[cur][c],l++;
if(l==n&&!calc[cur]) ans+=sum[cur],calc[cur]=1,stk.push(cur);

//cout << dbgs3(i,l,ans) << endl;
}
printf("%I64d\n",ans);
}


}

int n,m;
char s[N];

int work()
{
scanf("%s",s+1);
n=strlen(s+1);

SAM::init();
SAM::build_sam(s);

scanf("%d",&m);
while(m--)
{
scanf("%s",s+1);
n=strlen(s+1);
SAM::match(s);
}
return 0;
}

我整个人都自动机辣

作者

Bakapiano

发布于

2019-09-10

更新于

2020-03-28

许可协议

评论