后缀自动机题解

前言

后缀自动机(SAM)是一种优秀的数据结构,它作为一种自动机,不仅能接受一个字符串所有的后缀,还可以接受所有子串。然而这里并不打算写它的原理,这里仅提供题表和题解。
有关教程,固然,网上有很多详解,但都没有CLJ的正规,尽管CLJ的论文有些难懂,因此在学完网上的内容后,千万不要忘记回去看CLJ的论文。
进阶:Links

约定

  1. 状态:SAM上的点。
  2. 转移:SAM上的边。
  3. l o n g e s t longest longest:状态 i i i l o n g e s t longest longest 表示从初始状态转移到状态 i i i 的最多步数。
  4. s h o r t e s t shortest shortest:同 l o n g e s t longest longest,只是变成了最少步数。
  5. 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 集合的状态被合并到一个状态”这一点你是不知道的。
  6. 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 指针就会指向那个状态。
  7. f a i l fail fail 树:把SAN上的 f a i l fail fail 指针变成一条无向边,得到的就是 f a i l fail fail 树。

题表

题号(网站)名称(题解)题意提示
Spoj1811Longest Common Substring最长公共子串匹配
Spoj1812/bzoj2946Longest Common Substring II多个串最长公共子串取最小匹配
Spoj705/Spoj694Distinct Substrings子串个数DAG 上的 DP
Luogu P3804后缀自动机统计字符串fail 树上 DP
Spoj 8222Substrings统计字符串right 集合大小
Luogu P1368工艺最小表示法可以不用 SAM 或 SA
Spoj7258Lexicographical 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 longestshortest+1=longestfail.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=vson(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)} f1flength(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+vson(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=kfv,然后继续遍历。若 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后暴力,这里想说明的是,由于这道题的特殊性,还可以使用快速傅里叶变换解决,前提是你是个十分注意算法常数的人。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值