前言
后缀自动机(SAM)是一种优秀的数据结构,它作为一种自动机,不仅能接受一个字符串所有的后缀,还可以接受所有子串。然而这里并不打算写它的原理,这里仅提供题表和题解。
有关教程,固然,网上有很多详解,但都没有CLJ的正规,尽管CLJ的论文有些难懂,因此在学完网上的内容后,千万不要忘记回去看CLJ的论文。
进阶:Links
约定
- 状态:SAM上的点。
- 转移:SAM上的边。
- l o n g e s t longest longest:状态 i i i 的 l o n g e s t longest longest 表示从初始状态转移到状态 i i i 的最多步数。
- s h o r t e s t shortest shortest:同 l o n g e s t longest longest,只是变成了最少步数。
-
r
i
g
h
t
right
right 集合:节点
i
i
i 的
r
i
g
h
t
right
right 集合表示初始状态转移到节点
i
i
i 所表示的字符串在原串中出现的右端点集合。
如果你说初始状态转移到节点 i i i 可能有多条路径,那说明你还没有弄懂SAM,至少“相同 r i g h t right right 集合的状态被合并到一个状态”这一点你是不知道的。 -
f
a
i
l
fail
fail 指针:某个状态代表的是若干个
r
i
g
h
t
right
right 集合相同的串,那么随着后缀长度的减小,从某一个后缀开始,就可能出现在了更多的位置
而且这个后缀以及比它更短的后缀的 r i g h t right right 集合一定会变大,因此就不得不分离到另一个节点上,成为那个节点的 l o n g e s t longest longest,而当前状态的 f a i l fail fail 指针就会指向那个状态。 - f a i l fail fail 树:把SAN上的 f a i l fail fail 指针变成一条无向边,得到的就是 f a i l fail fail 树。
题表
题号(网站) | 名称(题解) | 题意 | 提示 |
---|---|---|---|
Spoj1811 | Longest Common Substring | 最长公共子串 | 匹配 |
Spoj1812/bzoj2946 | Longest Common Substring II | 多个串最长公共子串 | 取最小匹配 |
Spoj705/Spoj694 | Distinct Substrings | 子串个数 | DAG 上的 DP |
Luogu P3804 | 后缀自动机 | 统计字符串 | fail 树上 DP |
Spoj 8222 | Substrings | 统计字符串 | right 集合大小 |
Luogu P1368 | 工艺 | 最小表示法 | 可以不用 SAM 或 SA |
Spoj7258 | Lexicographical Substring Search | 第k小子串 | SAM 分治 |
Luogu P3763 | [TJOI2017]DNA | 允许3个例外的匹配 | fail 树上DFS |
模板
初始化
last=1,tot=1,ans=0;
memset(ch,0,sizeof ch);
memset(len,0,sizeof len);
memset(fails,0,sizeof fails);
构建函数 Θ ( n k ) \Theta(nk) Θ(nk)
void ins(int n){
int p=last,np=++tot;
last=np,len[np]=len[p]+1;
for(; p&&!ch[p][x];p=fail[p])
ch[p][x]=np;
if(p==0)
fail[np]=1;
else{
int q=ch[p][x];
if(len[q]==len[p]+1)
fail[np]=q;
else{
int nq=++tot;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof ch[q]);
fail[nq]=fail[q];
fail[q]=fail[np]=nq;
for(;ch[p][x]==q;p=fail[p])
ch[p][x]=nq;
}
}
}
SAM拓扑序 Θ ( n ) \Theta(n) Θ(n)
int in[maxn],t[maxn];
void tsort(){
int st=1,ed=1;
for(int i=1;i<=tot;i++)
for(int k=0;k<26;k++)
++in[ch[i][k]];
for(int i=1;i<=tot;i++)
if(!in[i])
t[ed++]=i;
while(st!=ed){
int &x=t[st++];
for(int k=0;k<26;k++)
if(ch[x][k]&&!--in[ch[x][k]])
t[ed++]=ch[x][k];
}
}
Fail树拓扑序 Θ ( n + k ) \Theta(n+k) Θ(n+k)
int ft[maxn],rs[maxn];
void build(){
const int n=strlen(s);
for(int i=1;i<=tot;i++)
rs[len[i]]++;
for(int i=n;i>=0;i--)
rs[i]+=rs[i+1];
for(int i=1;i<=tot;i++)
ft[rs[len[i]]--]=i;
}
题解
Spoj1811 Longest Common Substring
题意 求两个字符串的最长公共子串的长度。
题解 对第一个串建立后缀自动机,然后让第二个在上面跑即可。
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define maxn 1000000 //2n-1
int root=1,tot=1;
char s[maxn];
int fails[maxn],ch[maxn][30];
int last=1,len[maxn];
void ins(int x) {
int p=last,np=++tot;
last=np,len[np]=len[p]+1;
for(; p&&!ch[p][x]; p=fails[p])
ch[p][x]=np;
if(p==0)
fails[np]=1;
else {
int q=ch[p][x];
if(len[q]==len[p]+1)
fails[np]=q;
else {
int nq=++tot;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fails[nq]=fails[q];
fails[q]=fails[np]=nq;
for(;ch[p][x]==q; p=fails[p])
ch[p][x]=nq;
}
}
}
int runs(const char *s){
int cur=1,lens=0,ret=0;
for(int i=0;s[i];i++){
int x=s[i]-'a';
if(ch[cur][x]){
++lens;
cur=ch[cur][x];
}else{
while(cur&&!ch[cur][x])
cur=fails[cur];
if(!cur){
cur=1,lens=0;
}else{
lens=len[cur]+1;
cur=ch[cur][x];
}
}
ret=max(ret,lens);
}
return ret;
}
int main(void)
{
scanf("%s",s);
for(int i=0;s[i];i++)
ins(s[i]-'a');
scanf("%s",s);
printf("%d\n",runs(s));
return 0;
}
Spoj1812/bzoj2946 Longest Common Substring II
题意 求多个字符串最长公共子串的长度。
题解 对第一个字符串建立SAM,然后把每个串在上面跑,其中,答案记录在SAM的节点上,取所有匹配的最短匹配(公共),答案则为所有节点的最大匹配(最长)。
其实这样有个小小的问题,当到达了状态’aba’时,我们可能没有更新状态’ba’和状态’a’的答案。因此在每次加入一个串之后需要重新更新其fail树上的所有祖先的答案。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int maxn=500010;
int len[maxn],ch[maxn][26],fails[maxn];
int tot=1,last=1,maxs[maxn],ans[maxn];
void ins(int x) {
int p=last,np=++tot;
last=np,len[np]=len[p]+1;
for(; p&&!ch[p][x]; p=fails[p])
ch[p][x]=np;
if(p==0)
fails[np]=1;
else {
int q=ch[p][x];
if(len[q]==len[p]+1)
fails[np]=q;
else {
int nq=++tot;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fails[nq]=fails[q];
fails[q]=fails[np]=nq;
for(;ch[p][x]==q; p=fails[p])
ch[p][x]=nq;
}
}
}
int sum[maxn],tmp[maxn];
void Tsort(int n){//基数排序计算拓扑序
memset(sum,0,sizeof sum);
for(int i=1;i<=tot;i++)
sum[len[i]]++;
for(int i=1;i<=n;i++)
sum[i]+=sum[i-1];
for(int i=1;i<=tot;i++)
tmp[sum[len[i]]--]=i;
}
void work(const char *s){//匹配
memset(maxs,0,sizeof(maxs));
int lens=0,p=1;
for(int i=0;s[i];i++){
int x=s[i]-'a';
if(ch[p][x])//匹配成功
lens++,p=ch[p][x];//往下走
else{
for(;p&&!ch[p][x];p=fails[p]);//失配跳转
if(!p)
p=1,lens=0;
else
lens=len[p]+1,p=ch[p][x];
}
maxs[p]=max(maxs[p],lens);
}
for(int i=tot;i;i--){//更新fail树
int x=tmp[i];
ans[x]=min(ans[x],maxs[x]);
if(maxs[x]&&fails[x])
maxs[fails[x]]=len[fails[x]];
}
}
char s[maxn];
int main(){
scanf("%s",s);
for(int i=0;s[i];i++)
ins(s[i]-'a');
memset(ans,63,sizeof ans);
Tsort(strlen(s));
while(~scanf("%s",s))
work(s);
int res=0;
for(int i=1;i<=tot;i++)
res=max(res,ans[i]);
printf("%d\n",res);
return 0;
}
Spoj694/Spoj705 Distinct Substrings
题意 求一个字符串的不同的子串个数。
题解 对该字符串建立SAM。DP当然可以,但这里有一种更简单的方法。
既然每个状态表示的是若干个
r
i
g
h
t
right
right 相等的字符串,那么不难得出一点,对于一堆
r
i
g
h
t
right
right 集合相同的子串,它们一定互为后缀,并且他们长度连续。因此只需要考虑每个节点对答案的贡献即可,即不同的子串的个数为:
l
o
n
g
e
s
t
−
s
h
o
r
t
e
s
t
+
1
=
l
o
n
g
e
s
t
−
f
a
i
l
.
l
o
n
g
e
s
t
longest-shortest+1=longest-fail.longest
longest−shortest+1=longest−fail.longest
注意两道题目其中一道是大写字母,另一道是小写字母。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int maxn=500010;
int len[maxn],ch[maxn][26],fails[maxn];
int tot,last,ans;
void ins(int x) {
int p=last,np=++tot;
last=np,len[np]=len[p]+1;
for(; p&&!ch[p][x]; p=fails[p])
ch[p][x]=np;
if(p==0)
fails[np]=1;
else {
int q=ch[p][x];
if(len[q]==len[p]+1)
fails[np]=q;
else {
int nq=++tot;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fails[nq]=fails[q];
fails[q]=fails[np]=nq;
for(;ch[p][x]==q; p=fails[p])
ch[p][x]=nq;
}
}
}
char s[maxn];
int main(){
int n;
scanf("%d",&n);
while(n--&&~scanf("%s",s)){
last=1,tot=1,ans=0;
memset(ch,0,sizeof ch);
memset(len,0,sizeof len);
memset(fails,0,sizeof fails);
for(int i=0;s[i];i++)
ins(s[i]-'A');//705要变成 ins(s[i]-'a')
for(int i=1;i<=tot;i++)
ans+=len[i]-len[fails[i]];//直接统计答案
printf("%d\n",ans);
}
return 0;
}
Luogu P3804 后缀自动机
题意 求出字符串
S
S
S 的所有在
S
S
S 中出现次数不为
1
1
1 的子串的出现次数乘上该子串长度的最大值。
题解 构建出
S
A
M
SAM
SAM 后,可以发现每个字符串的出现次数就是对应状态的
r
i
g
h
t
right
right 集合大小。可以发现,在
f
a
i
l
fail
fail 树上,某个状态所表示的字符串一定是其儿子所表示的字符串的后缀,因此某个状态所表示的
r
i
g
h
t
right
right 一定是其所有儿子的
r
i
g
h
t
right
right 集合的并集。
又因为子串可以表示成某个后缀的前缀,因此只需要把原字符串的所有前缀的次数标记出来,然后在
f
a
i
l
fail
fail 树上合并即可,即:
s
i
z
e
u
=
∑
v
∈
s
o
n
(
u
)
s
i
z
e
v
size_u=\sum _{v\in son(u)}size_v
sizeu=v∈son(u)∑sizev 代码如下:
#include<bits/stdc++.h>
using namespace std;
#define maxn 2000010
char s[maxn];
int last=1,tot=1;
int len[maxn],ch[maxn][30],fails[maxn];
int size[maxn];
void ins(int x) {
int p=last,np=++tot;
last=np,len[np]=len[p]+1;
for(; p&&!ch[p][x]; p=fails[p])
ch[p][x]=np;
if(p==0)
fails[np]=1;
else {
int q=ch[p][x];
if(len[q]==len[p]+1)
fails[np]=q;
else {
int nq=++tot;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fails[nq]=fails[q];
fails[q]=fails[np]=nq;
for(;ch[p][x]==q; p=fails[p])
ch[p][x]=nq;
}
}
}
struct edge{
int v,next;
}edges[maxn];
int head[maxn];
void ins(int u,int v){
static int len=0;
edges[++len]=(edge){v,head[u]};
head[u]=len;
}
long long ans=0;
void dfs(int x){
for(int i=head[x];i;i=edges[i].next)
dfs(edges[i].v);
size[fails[x]]+=size[x];
if(size[x]>1)
ans=max(ans,(long long)size[x]*len[x]);
}
int main(void){
scanf("%s",s);
for(int i=0;s[i];i++)
ins(s[i]-'a');
for(int i=1;i<=tot;i++)
ins(fails[i],i);
for(int i=1,p=1;s[i];i++)
p=ch[p][s[i]-'a'],size[p]=1;
dfs(1);
printf("%lld\n",ans);
return 0;
}
这份代码只是幸运地在Linux系统下通过了而已,因为仔细想想可能会爆栈。于是我们可能需要一遍拓扑排序。其实不需要拓扑排序。
我们知道节点
v
v
v 的
r
i
g
h
t
right
right 集合一定是
v
v
v 的父亲
f
a
fa
fa 的
r
i
g
h
t
right
right 集合的子集,因此
v
.
l
o
n
g
e
s
t
v.longest
v.longest 必然大于
f
a
.
l
o
n
g
e
s
t
fa.longest
fa.longest。因此一个状态的
l
o
n
g
e
s
t
longest
longest 越长,它一定是更底层的状态。因此只需要对每个节点按照
l
o
n
g
e
s
t
longest
longest 排序即可,为了不提高时间复杂度,这里采用了基数排序(可参考后缀数组的倍增算法),和构建SAM的时间复杂度一致。
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define maxn 2000010
char s[maxn];
int last=1,tot=1;
int len[maxn],ch[maxn][30],fails[maxn];
int size[maxn];
void ins(int x) {
int p=last,np=++tot;
last=np,len[np]=len[p]+1;
for(; p&&!ch[p][x]; p=fails[p])
ch[p][x]=np;
if(p==0)
fails[np]=1;
else {
int q=ch[p][x];
if(len[q]==len[p]+1)
fails[np]=q;
else {
int nq=++tot;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fails[nq]=fails[q];
fails[q]=fails[np]=nq;
for(;ch[p][x]==q; p=fails[p])
ch[p][x]=nq;
}
}
}
int t[maxn],rs[maxn];
void build(){
const int n=strlen(s);
for(int i=0;i<n;i++)
ins(s[i]-'a');
for(int i=1;i<=tot;i++)
rs[len[i]]++;
for(int i=n;i>=0;i--)
rs[i]+=rs[i+1];
for(int i=1;i<=tot;i++)
t[rs[len[i]]--]=i;
}
long long ans=0;
void solve(){
for(int i=1,p=1;s[i];i++)
p=ch[p][s[i]-'a'],size[p]=1;
for(int i=1;i<=tot;i++){
int now=t[i];
size[fails[now]]+=size[now];
if(size[now]>1)
ans=max(ans,(long long)size[now]*len[now]);
}
}
int main(void){
scanf("%s",s);
build();
solve();
printf("%lld\n",ans);
return 0;
}
Spoj 8222 Substrings
题意 定义
f
i
f_i
fi 为字符串
S
S
S 的所有长度为
i
i
i 的子串的出现次数的最大值。求
f
1
⋯
f
l
e
n
g
t
h
(
S
)
f_{1}\cdots f_{length(S)}
f1⋯flength(S) 值。
题解 有了上一题的基础,这一题应该不难解决。先对
S
S
S 建立SAM,对于节点
v
v
v,可以发现,其对
f
v
.
s
h
o
r
t
e
s
t
,
⋯
,
f
v
.
l
o
n
g
e
s
t
f_{v.shortest},\cdots,f_{v.longest}
fv.shortest,⋯,fv.longest均有贡献,如果这样维护那时间复杂度就是
O
(
n
2
)
O(n^2)
O(n2) 的了。
其实可以发现,长度为
i
i
i 的子串一定是长度为
i
+
1
i+1
i+1 的子串的子串。
也就是说,我们可以只考虑
f
v
.
l
o
n
g
e
s
t
f_{v.longest}
fv.longest ,然后用
f
i
=
m
a
x
(
f
i
+
1
,
f
i
)
f_i=max(f_{i+1},f_i)
fi=max(fi+1,fi) 来更新更短的子串。
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define maxn 2000010
char s[maxn];
int last=1,tot=1;
int len[maxn],ch[maxn][30],fails[maxn];
int size[maxn];
int t[maxn],rs[maxn];
void ins(int x) {
int p=last,np=++tot;
last=np,len[np]=len[p]+1;
for(; p&&!ch[p][x]; p=fails[p])
ch[p][x]=np;
if(p==0)
fails[np]=1;
else {
int q=ch[p][x];
if(len[q]==len[p]+1)
fails[np]=q;
else {
int nq=++tot;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fails[nq]=fails[q];
fails[q]=fails[np]=nq;
for(;ch[p][x]==q; p=fails[p])
ch[p][x]=nq;
}
}
}
void build(){
const int n=strlen(s);
for(int i=0;i<n;i++)
ins(s[i]-'a');
for(int i=1;i<=tot;i++)
rs[len[i]]++;
for(int i=n;i>=0;i--)
rs[i]+=rs[i+1];
for(int i=1;i<=tot;i++)
t[rs[len[i]]--]=i;
}
long long f[maxn];
int main(void){
scanf("%s",s);
build();
const int n=strlen(s);
for(int i=0,p=1;s[i];i++)
p=ch[p][s[i]-'a'],size[p]=1;
for(int i=1;i<=tot;i++)
size[fails[t[i]]]+=size[t[i]];
for(int i=1;i<=tot;i++)
f[len[i]]=max(f[len[i]],(long long)size[i]);
for(int i=n;i;i--)//似乎数据水,不加也能过
f[i]=max(f[i],f[i+1]);
for(int i=1;i<=n;i++)
printf("%lld\n",f[i]);
return 0;
}
Luogu P1368 工艺
题意 给定一个循环序列,从某处断开,输出所有可能得到的序列中,字典序最小的那一个。
题解 对于循环类的问题,先把序列复制一遍,然后构建SAM,这里有个小问题,不知道每个元素的大小,导致
c
h
ch
ch 数组不好开,这里其实可以用
m
a
p
map
map。
建立好SAM之后,可以直接从初始状态出发,贪心地沿着最小的边(ch[p].begin()
)走,走
n
n
n 步即可。
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define maxn 1000010
char s[maxn];
int last=1,tot=1;
int len[maxn],fails[maxn];
map<int,int> ch[maxn];
int size[maxn];
void ins(int x){
int p=last,np=++tot;
last=np;len[np]=len[p]+1;
for(;p&&!ch[p].count(x);p=fails[p])
ch[p][x]=np;
if(!p)
fails[np]=1;
else{
int q=ch[p][x];
if(len[q]==len[p]+1)
fails[np]=q;
else{
int nq=++tot;
len[nq]=len[p]+1;
ch[nq]=ch[q];
fails[nq]=fails[q];
fails[q]=fails[np]=nq;
for(;ch[p][x]==q;p=fails[p])
ch[p][x]=nq;
}
}
}
int n,tt[maxn];
int main(void)
{
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&tt[i]);
ins(tt[i]);
}
for(int i=1;i<=n;i++)
ins(tt[i]);
for(int p=1,i=1;i<=n;i++){
map<int,int>::iterator pos=ch[p].begin();
printf("%d ",pos->first);
p=pos->second;
}
printf("\n");
return 0;
}
又及 这道题还可以用后缀数组(SA)解决,方法类似,倍长后第一个长度大于等于 n n n 的后缀即为答案,时间复杂度比SAM略高。可这不是重点,重点是这题有绝对的 Θ ( n ) \Theta(n) Θ(n) 的时间复杂度。算法名称:最小表示法。
Spoj7258 Lexicographical Substring Search
题意 给出一个字符串,若相同子串算一次,且排名相同,询问其字典序第
k
k
k 小的子串。
题解 由SAM的性质得,所有
r
i
g
h
t
right
right 集合相同的子串会被合并到一个状态中,那完全相同的子串就更加会被合并到一个状态中了。定义
f
u
f_u
fu 表示从状态
u
u
u 出发,能到达的的串的个数(且这些串一定是原字符串的子串),则有:
f
u
=
1
+
∑
v
∈
s
o
n
(
u
)
f
v
f_u=1+\sum_{v\in son(u)}f_v
fu=1+v∈son(u)∑fv 注意这个方程需要的计算顺序。按照SAM的拓扑序固然可以,但其实也可以按照fail树的拓扑序。为什么?其实本来是不可以的,但是因为我们做fail树的拓扑排序是根据其len的大小排序的,因而更长的串一定先被处理了。
得到
f
f
f 后就很容易了,从初始状态
s
s
s 出发,带上
k
k
k,扫一遍
s
s
s 的儿子,设当前访问到的儿子为
v
v
v,若
f
v
>
k
f_v>k
fv>k,则答案必然不在
v
v
v 子树中,令
k
=
k
−
f
v
k=k-f_v
k=k−fv,然后继续遍历。若
f
v
<
=
k
f_v<=k
fv<=k,则令
s
=
v
s=v
s=v 进入该子树找。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int ch[maxn][26],fail[maxn],len[maxn],sum[maxn];
int n,last=1,tot=1;
char s[maxn];
void extend(int x){
int p=last,np=++tot;
last=np;len[np]=len[p]+1;
for(;p&&!ch[p][x];p=fail[p])
ch[p][x]=np;
if (p==0)
fail[np]=1;
else{
int q=ch[p][x];
if(len[q]==len[p]+1)
fail[np]=q;
else{
int nq=++tot;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[nq]));
fail[nq]=fail[q];
fail[q]=fail[np]=nq;
for(;p&&ch[p][x]==q;p=fail[p])
ch[p][x]=nq;
}
}
}
int ft[maxn],rs[maxn];
void build(){
const int n=strlen(s);
for(int i=1;i<=tot;i++)
rs[len[i]]++;
for(int i=n;i>=0;i--)
rs[i]+=rs[i+1];
for(int i=1;i<=tot;i++)
ft[rs[len[i]]--]=i;
}
void solve(int k){
int p=1;
while(k>0){
for(int j=0;j<26;++j){
if(!ch[p][j])
continue;
if(sum[ch[p][j]]>=k){
putchar(j+'a');
--k;
p=ch[p][j];
break;
}else
k-=sum[ch[p][j]];
}
}
putchar('\n');
}
int main(){
scanf("%s",s);
for(int i=0;s[i];++i)
extend(s[i]-'a');
build();//同样可以用拓扑排序tsort
for(int i=1;i<=tot;i++){
sum[ft[i]]=1;
for(int j=0;j<26;++j)
sum[ft[i]]+=sum[ch[ft[i]][j]];
}
int Q,k;
scanf("%d",&Q);
while (Q--&&~scanf("%d",&k))
solve(k);
return 0;
}
Luogu P3763 [TJOI2017]DNA
题意 给定两个 DNA 序列
S
S
S 和
T
T
T,求允许 3 个例外字符的情况下,
T
T
T 在
S
S
S 中出现的次数。
题解 考虑生物:DNA 序列中只包含
A
,
T
,
C
,
G
A,T,C,G
A,T,C,G 四种字符,否则会浪费很多空间。对
S
S
S 建立后缀自动机,求出每个节点的
r
i
g
h
t
right
right 集合大小,然后直接在 SAM 上暴力搜索即可。
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define maxn 200010
#define N 100010
int root,tot,ans;
char s[N];
int fails[maxn],ch[maxn][5],cnt[maxn];
int last,len[maxn];
inline void init() {
root=last=tot=1,ans=0;
memset(ch,0,sizeof ch);
memset(len,0,sizeof len);
memset(fails,0,sizeof fails);
memset(cnt,0,sizeof cnt);
}
inline void ins(int x) {
int p=last,np=++tot;
last=np,len[np]=len[p]+1;
for(; p&&!ch[p][x]; p=fails[p])
ch[p][x]=np;
if(p==0)
fails[np]=1;
else {
int q=ch[p][x];
if(len[q]==len[p]+1)
fails[np]=q;
else {
int nq=++tot;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof ch[q]);
fails[nq]=fails[q];
fails[q]=fails[np]=nq;
for(; ch[p][x]==q; p=fails[p])
ch[p][x]=nq;
}
}
}
int ft[maxn],rs[N];
inline void build() {
const int n=strlen(s)+10;
for(register int i=1; i<=tot; ++i)
rs[len[i]]++;
for(register int i=n; i>=0; i--)
rs[i]+=rs[i+1];
for(register int i=1; i<=tot; ++i)
ft[rs[len[i]]--]=i;
for(register int i=n; i>=0; --i)
rs[i]=0;
}
int Ts[127];
inline void right() {
for(register int i=0,p=1; s[i]; i++)
p=ch[p][(int)s[i]],cnt[p]=1;
for(register int i=1; i<=tot; i++)
cnt[fails[ft[i]]]+=cnt[ft[i]];
}
char str[N];int m;
void dfs(int p,int len,int dec){
if(len>=m){
ans+=cnt[p];
return;
}
for(register int i=1;i<=4;++i){
if(!ch[p][i]) continue;
if(i==str[len])
dfs(ch[p][i],len+1,dec);
else if(dec<3)
dfs(ch[p][i],len+1,dec+1);
}
}
int main(void) {
int T;
Ts['A'-'A']=1;Ts['T'-'A']=2;Ts['C'-'A']=3;Ts['G'-'A']=4;
scanf("%d",&T);
while(T--){
init();
scanf("%s%s",s,str);
for(register int i=0;s[i];++i)
ins(s[i]=Ts[s[i]-'A']);
for(m=0;str[m];m++)
str[m]=Ts[str[m]-'A'];
build();right();
dfs(1,0,0);
printf("%d\n",ans);
}
return 0;
}
又及 本题的最优解为哈希,但并非完美算法,同样也可以用SA求出LCP后暴力,这里想说明的是,由于这道题的特殊性,还可以使用快速傅里叶变换解决,前提是你是个十分注意算法常数的人。