字符串1

前置知识

  • \(S[l,r]\) 表示字符串 \(S\)\(S_l,S_{l+1},...S_{r-1},S_r\) 构成的字串。
  • \(|S|\) 表示 \(S\) 的长度。

马拉车(manacher)

P3805 【模板】manacher

几乎所有的字符串算法都有着使用已知的信息去优化新信息的获得。

\(p_x\) 为以 \(x\) 为对称中心的最长回文长度,我们先考虑回文长度为奇数。

假设我们已经求出了前 \(x-1\) 个信息,并且已经知道了 \(S[l,r]\) 为回文串 \(l\le x\le r\),这可以进一步确认为 \(mid=\frac{l+r}2\le x\le r\)。如下图,\(y\)\(x\) 关于 \(mid\) 的对称位置:

由于回文串的对称性,显然有 \(p_x\ge p_y\),如果 \(p_x>p_y\),当且仅当 \(p_x\) 可以延伸到 \(R\) 以外。

我们考虑一种算法,维护目前最大的 \(r\),如果当前 \(i\) 小于等于 \(r\),那么将 \(i\) 的答案设为其关于 \(mid\) 的对称点的答案,然后再暴力向外拓展。这就是 manacher,复杂度为 \(O(n)\)

感性理解一下,如果 \(x\) 的答案需要拓展,那么必然造成 \(r\) 的更新,因此 \(r\) 是单调不减的,也因此复杂度是正确的。

对于回文长度为偶数我们在每个字符之间添上一个 |,转化为奇数,最终将答案除以 \(2\) 即可。,当然如果你求的是最大的 \(p_i\) 那么除以 \(2\) 刚好省去。

注意要在两边添加奇怪符号防止出现奇怪现象。

#include<bits/stdc++.h>
using namespace std;
const int N=2.2e7+5;
char s[N];
int p[N];
int len,ans;
signed main(){
	char ch=getchar();
	s[0]='?',s[len=1]='|';
	while(ch<'a'||ch>'z')ch=getchar();
	while(ch>='a'&&ch<='z')s[++len]=ch,s[++len]='|',ch=getchar();
	for(int i=1,j=0,mid=0;i<=len;++i){
		if(i<=j)p[i]=min(p[mid*2-i],j-i+1);
		while(s[i-p[i]]==s[i+p[i]])++p[i];
		if(i+p[i]>j)j=i+p[i]-1,mid=i;
		ans=max(ans,p[i]);
	}cout<<ans-1;
	return 0;
}

P4555 [国家集训队] 最长双回文串

先跑一遍 manacher 是没问题的。

现在要求出每个位置左边最长回文和右边最回文。这里以求右边为例。位置 \(i\)\(i-p_i+1\) 的贡献为 \(2p_i-1\),对 \(i-p_i+2\) 的贡献为 \(2p_i-3\) 等等。我们发现只用更新 \(i-p_i+1\),然后向后推一遍最大值即可。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int t,n,ans;
char s[N];
int p[N],l[N],r[N];
inline void init(){
	char ch=getchar();s[0]='?',s[n=1]='|';
	while(ch<'a'||ch>'z')ch=getchar();
	while(ch>='a'&&ch<='z')s[++n]=ch,s[++n]='|',ch=getchar();
	s[n+1]='*';
}
signed main(){
	init();
	for(int i=1,j=0,mid=0;i<=n;++i){
		if(i<=j)p[i]=min(p[mid*2-i],j-i+1);
		while(s[i-p[i]]==s[i+p[i]])++p[i];
		if(i+p[i]>j)j=i+p[i]-1,mid=i;
	}
	for(int i=n;i>=1;--i)
		l[i+p[i]-1]=max(l[i+p[i]-1],p[i]-1);
	for(int i=n;i>=3;i-=2)
		l[i]=max(l[i],l[i+2]-2);
	for(int i=1;i<=n;++i)
		r[i-p[i]+1]=max(r[i-p[i]+1],p[i]-1);
	for(int i=3;i<=3;i+=2)
		r[i]=max(r[i],r[i-2]-2);
	for(int i=3;i<=n;i+=2)
	    if(l[i]&&r[i])ans=max(ans,l[i]+r[i]);
	cout<<ans;
	return 0;
}

P1659 [国家集训队] 拉拉队排练

不用加分隔符,直接跑一遍 manacher,因为只有求奇数。因为 manacher 贡献的长度为 \(2p_i-1\) 的回文串,必然贡献长度 \(2p_i-3\) 的等等。因此做一个差分,然后快速幂计算即可。

P5446 [THUPC2018] 绿绿和串串

\(R\) 一定是 \(S\) 的前缀。位置 \(i\) 合法的条件为他可以翻转超出 \(S\) 或者翻转为一个可以翻转成功的字符串。

#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5;
int t,n;
int p[N],ans[N];
char s[N];
inline void init(){
	char ch=getchar();s[0]='?',s[n=1]='|';
	while(ch<'a'||ch>'z')ch=getchar();
	while(ch>='a'&&ch<='z')s[++n]=ch,s[++n]='|',ch=getchar();
	s[n+1]='*';
}
signed main(){
	cin>>t;
	while(t--){
		init();
		for(int i=1,j=0,mid=0;i<=n;++i){
			if(i<=j)p[i]=min(p[mid*2-i],j-i+1);
			while(s[i-p[i]]==s[i+p[i]])++p[i];
			if(i+p[i]>j)j=i+p[i]-1,mid=i;
		}
		for(int i=n;i>=1;--i){
			if(i+p[i]-1==n)ans[i]=1;
			if(i-p[i]+1==1&&ans[i+p[i]-2])ans[i]=1;
		}
		for(int i=2;i<=n;i+=2)
			if(ans[i])
				cout<<(i>>1)<<' ';
		cout<<'\n';
		for(int i=1;i<=n;++i)
			ans[i]=p[i]=0;
	}
	return 0;
}

P6216 回文匹配

\(s2\) 出现的位置设为权值 \(1\),对于一个 \(i\),我们会要计算 \([i-p_i+1,i+p_i-m],[i-p_i+2,i+p_i-m-1]...\) 的和(因为 \(s2\) 要完全在内)。然后就是二次前缀和了。

#include<bits/stdc++.h>
using namespace std;
const int N=3e6+5;
int n,m;
string s1,s2;
int p[N],kmp[N];
int sum[N];
unsigned int ans=0;
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m>>s1>>s2;
	s1="!"+s1+"#";
	s2="$"+s2+"%";
	for(int i=2,j=0;i<=m;++i){
		while(j&&s2[i]!=s2[j+1])j=kmp[j];
		if(s2[i]==s2[j+1])++j;kmp[i]=j;
	}
	for(int i=1,j=0;i<=n;++i){
		while(j&&s1[i]!=s2[j+1])j=kmp[j];
		if(s1[i]==s2[j+1])++j;
		if(j==m)++sum[i-m+1],j=kmp[j];
	}
	for(int i=1;i<=n;++i)sum[i]+=sum[i-1];
	for(int i=1;i<=n;++i)sum[i]+=sum[i-1];
	for(int i=1,j=0,mid=0;i<=n;++i){
		if(i<=j)p[i]=min(p[mid*2-i],j-i+1);
		while(s1[i-p[i]]==s1[i+p[i]])++p[i];
		if(i+p[i]>j)j=i+p[i]-1,mid=i;
	}
	for(int i=1;i<=n;++i){
		if(p[i]*2-1<m)continue;
		int l=i-p[i],r=i+p[i]-m,mid=(l+r)>>1;
		ans+=sum[r]-sum[mid]-sum[(l+r)&1?mid:mid-1]+sum[l-1];
	}cout<<ans;
	return 0;
}

KMP,exKMP

P3375 【模板】KMP

KMP 重要的不是为了匹配,因为可以轻松用哈希代替,而是它为我们提供了有用的 \(nxt\) 数组。

\(nxt_i\) 的定义是 \(s2[1,i]\) 的最长真相同前后缀,实际作用是告诉我们加入我们匹配到 \(s1\) 的第 \(i\) 位,\(s2\) 的第 \(j+1\) 位,此时失配,我们使 \(j\gets nxt_j\) 继续匹配。原理读者可以借助下图理解,红色为 \(nxt\),由于红色部分是相同的,所以无需继续重新匹配。

对于如何求 \(nxt\),我们相当于对 \(s2\) 自己做依次匹配。

复杂度是 \(O(n)\) 的,可以感性理解为 \(j\) 相对于 \(i\) 的位置不减小。

#include<bits/stdc++.h>
using namespace std;
const int N=1000005;
string s1,s2;
int kmp[N];
signed main(){
	std::ios::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);
	cin>>s1>>s2;
	register int i,j=0,len1=s1.size(),len2=s2.size();
	s1=" "+s1+" ";
	s2=" "+s2+" ";
	for(i=2;i<=len2;i++){
		while(j&&s2[i]!=s2[j+1])
			j=kmp[j];
		if(s2[i]==s2[j+1])
			j++;
		kmp[i]=j;
	}
	j=0;
	for(i=1;i<=len1;i++){
		while(j&&s1[i]!=s2[j+1])
			j=kmp[j];
		if(s1[i]==s2[j+1])
			j++;
		if(j==len2){
			cout<<i-len2+1<<'\n';
			j=kmp[j];
		}
	}
	for(i=1;i<=len2;i++)
		cout<<kmp[i]<<' ';
	return 0;
}

P3435 [POI2006] OKR-Periods of Words

会发现题目所说的周期特像 \(nxt\) 的定义。

举个例子 abccbaabc,可以看出其最长“周期”为 abccba,实际上我们是将原串分成了 abccba+abc,会发现后面是匹配的。

换句话说,我们要求出每个前缀的最短公共前后缀,这样使得“周期”足够长。于是我们从 KMP 的 \(nxt\) 数组出发,不断令 \(i\gets nxt_i\),直到 \(nxt_i=0\)。但是复杂度有点问题,用一个类似并查集的路径压缩即可。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,kmp[N];
string s;
long long ans;
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>s;s=" "+s;
	for(int i=2,j=0;i<=n;++i){
		while(j&&s[i]!=s[j+1])j=kmp[j];
		if(s[i]==s[j+1])++j;kmp[i]=j;
	}
	for(int i=1;i<=n;++i){
		int j=i;
		while(kmp[j])j=kmp[j];
		if(kmp[i])kmp[i]=j;
		ans+=i-j;
	}cout<<ans;
	return 0;
}

P2375 [NOI2014] 动物园

\(num_i\) 为从 \(i\) 开始不断令 \(i\gets nxt_i\) 能做多少次。对于每个位置的答案,我们只用让不断使 \(j\gets nxt_j\) 直到 \(j\le \frac i2\),此时 \(num_j\) 就是这个位置的答案。

注意不可以在一开始就限制 \(nxt\)

P3193 [HNOI2008] GT考试

先考虑朴素做法,\(f_{i,j}\) 表示第 \(i\) 位匹配到 \(A_j\) 的方案数,转移很显然。

看到数据范围上肯定上矩阵,对于这个矩阵的构造,我们要知道这个位置填上某个数之后后缀与 \(A\) 的前缀最长的一个,于是显然的 KMP。

#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5,INF=0x3f3f3f3f;
int n,m,P;string s;
struct mat{
	int a[23][23];
	void init(int x){
		memset(a,0,sizeof(a));
		for(int i=0;i<m;++i)
			a[i][i]=x;
	}
}g;
inline mat operator *(mat x,mat y){
	mat z;z.init(0);
	for(int i=0;i<m;++i)
		for(int j=0;j<m;++j)
			for(int k=0;k<m;++k)
				(z.a[i][j]+=x.a[i][k]*y.a[k][j])%=P;
	return z;
}
inline mat qp(mat x,int y){
	mat ans;ans.init(1);
	for(;y;y>>=1,x=x*x)
		if(y&1)ans=ans*x;
	return ans;
}int kmp[23];
void Kmp(){
	for(int i=2,j=0;i<=m;++i){
		while(j&&s[i]!=s[j+1])j=kmp[j];
		if(s[i]==s[j+1])++j;
		kmp[i]=j;
	}
	for(int i=0;i<m;++i){
		for(int c='0';c<='9';++c){
			int j=i;
			while(s[j+1]!=c&&j)j=kmp[j];
			if(s[j+1]==c)++j;
			++g.a[i][j];
		}
	}
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m>>P>>s,s=" "+s,Kmp();
	mat res;res.init(0);res.a[0][0]=1;
	res=res*qp(g,n);int ans=0;
	for(int i=0;i<m;++i)(ans+=res.a[0][i])%=P;
	cout<<ans<<'\n';
	return 0;
}

CF526D Om Nom and Necklace

考虑我们的 \(nxt\) 数组。

如果我们的字符串为 WYWYWYWYWY 的形式,\(nxt\) 找出来一定是这样。

WYWYWYWYWY
WYWYWYWY
  WYWYWYWY

如果是 WYWYWYWYW 的形式,找出来一定是这样。

WYWYWYWYW
WYWYWYW
  WYWYWYW

因此 \(i-nxt_i\) 就是长度最短的 WY。对于第一种情况,假设我们有 \(l\)WY,我们将 \(l\bmod k\)WY 作为 \(A\)\(\lfloor \frac l k\rfloor -l\bmod k\)WY 作为 \(B\),判断 \(|B|\) 是否 \(\ge0\)。对于第二种情况,假设我们有 \(l\)WY(将最后一个 \(W\) 除掉),我们将 W 外加 \(l\bmod k\)YW 作为 \(A\)Y 外加 \(\lfloor \frac l k\rfloor -l\bmod k-1\)WY 作为 \(B\),判断 \(\lfloor \frac l k\rfloor -l\bmod k-1\) 正负性即可。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,k,kmp[N];
char a[N];
signed main(){
    cin>>n>>k;
    scanf("%s",a+1);
    for(int i=2,j=0;i<=n;++i){
        while(j&&a[i]!=a[j+1])j=kmp[j];
        if(a[i]==a[j+1])++j;kmp[i]=j;
    }
    for(int i=1;i<=n;++i){
        int j=i-kmp[i],l=i/j;
        if(i%j>0)cout<<(l/k>l%k);
        else cout<<(l/k>=l%k);
    }
    return 0;
}

P3546 [POI2012] PRE-Prefixuffix

考虑循环相同的的两个串一定是 \(AB,BA\) 的形式。对于 \(A\),我们可以求出 \(s\) 的 border。现在我们要求出 \(s[i+1,n-i]\) 的最长 border,一个显然结论就是设 \(f(i)\)\(s[i+1,n-i]\) 最长 border 的长度,有 \(f(i)\le f(i+1)+2\),于是倒推 hash 就可以 \(O(n)\) 求出。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5,B=290023,P=1e9+7;
char s[N];
int n,pw[N],ha[N];
int kmp[N],f[N],ans;
int calcium(int l,int r){
	return (ha[r]-ha[l-1]*pw[r-l+1]%P+P)%P;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>s+1;
	pw[0]=1,pw[1]=B,ha[1]=s[1];
	for(int i=2,j=0;i<=n;++i){
		while(j&&s[i]!=s[j+1])j=kmp[j];
		kmp[i]=j+=s[i]==s[j+1];
		pw[i]=pw[i-1]*B%P;
		ha[i]=(ha[i-1]*B+s[i])%P;
	}for(int i=n/2;i;--i){
		f[i]=f[i+1]+2;
		while(f[i]){
			if((i+f[i])*2>n)--f[i];
			else if(calcium(i+1,i+f[i])!=calcium(n-i-f[i]+1,n-i))
				--f[i];
			else break;
		}
	}for(int i=kmp[n];i;i=kmp[i])
		if(i*2<=n)
			ans=max(ans,i+f[i]);
	cout<<ans<<'\n';
	return 0;
}

P3426 [POI2005] SZA-Template

首先,如果 \(s\) 能被某印章完成(印章不为 \(s\)),那么 \(s\) 的 border 也可以。

于是显然先跑一遍 KMP,考虑递推。设 \(f(i)\)\(s[1,i]\) 的答案。\(f(i)\) 可能就是 \(i\),要么就是 \(f(nxt_i)\),但是 \(f(nxt_i)\) 不一定都可取。考虑所有 \(f(j)=f(nxt_i)\) 用的一定是同一个印章,现在我们就看那个印章能不能印到 \(i\) 这个位置,于是我们保存一个最大的 \(j\) 使得 \(f(j)=f(nxt_i)\),判断是否 \(j+f(nxt_i)\ge i\),如果是就可取。

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,kmp[N],f[N],g[N];
string s;
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>s,n=s.size(),s=" "+s;
	for(int i=2,j=0;i<=n;++i){
		while(j&&s[i]!=s[j+1])j=kmp[j];
		kmp[i]=j+=s[i]==s[j+1];
	}g[f[1]=1]=1;
	for(int i=2;i<=n;++i)
		g[f[i]=(g[f[kmp[i]]]>=i-f[kmp[i]]?f[kmp[i]]:i)]=i;
	cout<<f[n]<<'\n';
	return 0;
}

P5410 【模板】扩展 KMP/exKMP(Z 函数)

注意 exKMP 在不卡 \(O(n\log n)\) 的情况下可以用哈希轻松解决。

感觉像是 manacher 和 KMP 的结合版。

假如我们求出的一个匹配的答案长这样,红色为相同的部分。

假设我们要求以蓝色为起点的答案,我们显然不用从头找,只用从右边红色右端开始找就行了。复杂度也可以感性理解,\(O(n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=2e7+5;
int n,m,nxt[N],ext[N];
char a[N],b[N];
long long ans;
signed main(){
    scanf("%s %s",a+1,b+1);
    n=strlen(a+1),m=strlen(b+1);
    nxt[1]=m;
    for(int i=2,l=0,r=0;i<=m;++i){
        if(i<=r)nxt[i]=min(nxt[i-l+1],r-i+1);
        while(i+nxt[i]<=m&&b[i+nxt[i]]==b[nxt[i]+1])++nxt[i];
        if(i+nxt[i]-1>r)l=i,r=i+nxt[i]-1;
    }
    for(int i=1,l=0,r=0;i<=n;++i){
        if(i<=r)ext[i]=min(nxt[i-l+1],r-i+1);
        while(i+ext[i]<=n&&a[i+ext[i]]==b[ext[i]+1])++ext[i];
        if(i+ext[i]-1>r)l=i,r=i+ext[i]-1;
    }
    for(int i=1;i<=m;++i)
        ans^=1ll*i*(nxt[i]+1);
    cout<<ans<<'\n';ans=0;
    for(int i=1;i<=n;++i)
        ans^=1ll*i*(ext[i]+1);
    cout<<ans<<'\n';ans=0;
    return 0;
}

P5829 【模板】失配树

\(nxt\) 数组相当于树上的 \(fa\) 一样,找 Border 相当于一直 \(p\gets nxt_p,q\gets nxt_q\)(迭代次数不一定相同)使得 \(nxt_p=nxt_q\),于是从 $nxt_i\to i $ 连一条边,可得最终是一棵树,最终求 lca 即可。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,m;char a[N];
int f[N][21],dep[N];
signed main(){
    scanf("%s",a+1);n=strlen(a+1);
    dep[1]=1;
    for(int i=2,j=0;i<=n;++i){
        while(j&&a[i]!=a[j+1])j=f[j][0];
        if(a[i]==a[j+1])++j;
        f[i][0]=j,dep[i]=dep[j]+1;
    }
    for(int j=1;j<=20;++j)
        for(int i=1;i<=n;++i)
            f[i][j]=f[f[i][j-1]][j-1];
    scanf("%d",&m);
    while(m--){
        int u,v;scanf("%d%d",&u,&v);
        if(dep[u]<dep[v])swap(u,v);
        for(int i=20;i>=0;--i)
            if(dep[f[u][i]]>=dep[v])
                u=f[u][i];
        for(int i=20;i>=0;--i)
            if(f[u][i]!=f[v][i])
                u=f[u][i],v=f[v][i];
        cout<<f[u][0]<<'\n';
    }
    return 0;
}

AC 自动机

如果查询多个字符串在 \(s\) 中是否出现/出现几次,虽然这些串加起来长度不超过 \(10^5\) 但是过多的数量使得复杂度会掉到 \(O(n^2)\)

多个字符串会使人想到 trie,模仿 KMP 的方式,对于每个状态 \(i\),即字典树一直走到这个位置的字符串,\(fail_{i,j}\) 表示 \(j\) 没有匹配上,现在字符串的最长真后缀的位置,注意 \(fail_{i,j}\) 可能为空。

比如字典树 \(\{\mathrm{babac},\mathrm{aba}\}\)\(\mathrm{ba}\) 会指向 \(\mathrm{a}\)\(\mathrm{aba}\) 会指向 \(\mathrm{ba}\)\(\mathrm{baba}\) 会指向 \(\mathrm{aba}\),但是 \(\mathrm{babac}\) 就指向 \(root\) 了。

如何求解 \(fail_i\),先使 \(fail_i\gets fail_{fa_i}\),如果此时 \(fail_i\) 没有指向 \(fa_i\to i\) 这条边表示的字符的话继续使 \(fail_i\gets fail_{fail_i}\),直到 \(root\) 为止。

但是我们显然不能这样去暴力,考虑先将所有字符串先插到字典树中,然后一齐建出来。具体地,使用 bfs,假设我们要求 ch[i][j]\(fail\),设这个答案为 \(f(i,j)\),则。

\[f(i,j)=\begin{cases} ch_{i,j}&\text{if}\ch_{i,j}\not=0 \\ 0 &\text{if}\ i=root\land ch_{i,j}=0\\ f(fail_i,j)&\text{if}\ i\not=root\land ch_{i,j}=0 \end{cases}\]

int n,to[N];
int ch[N][26],ne[N],tot;
inline int insert(string s){
	int p=0,len=(int)s.size();
	for(int i=0;i<len;++i){
		int j=s[i]-'a';
		if(!ch[p][j])ch[p][j]=++tot;
		p=ch[p][j];
	}return p;
}
inline void build(){
	queue<int>q;
	for(int i=0;i<26;++i)
		if(ch[0][i])
			ne[ch[0][i]]=0,q.push(ch[0][i]);
	while(q.size()){
		int u=q.front();q.pop();
		for(int i=0;i<26;++i){
			int v=ch[u][i],w=ch[ne[u]][i];
			if(v)ne[v]=w,q.push(v);
			else ch[u][i]=w;
		}
	}
}

P3808 AC 自动机(简单版)

建好自动机以后,对于 \(s\) 匹配到的字典树每个位置,一直使 \(i\gets fail_i\) 统计答案。

早期奇怪马蜂

#include<bits/stdc++.h>
using namespace std;
inline void init(){
	std::ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
}const int N=1000005;
int n,ne[N];
int ch[N][26],cnt[N],tot;
void insert(string s){
	int p=0,len=s.size(),i=0,j=0;
	for(i=0;i<len;i++){
		j=s[i]-'a';
		if(!ch[p][j])
			ch[p][j]=++tot;
		p=ch[p][j];
	}
	cnt[p]+=1;
}void build(){
	int i,j,u,v;
	queue<int> q;
	for(i=0;i<26;i++)
		if(ch[0][i])
			ne[ch[0][i]]=0,q.push(ch[0][i]);
	while(q.size()){
		u=q.front();q.pop();
		for(i=0;i<26;i++){
			v=ch[u][i];
			if(v)
				ne[v]=ch[ne[u]][i],q.push(v);
			else
				ch[u][i]=ch[ne[u]][i];
		}
	}
}int query(string s){
	int ans=0,p=0,i=0,j=0;
	for(i=0;i<s.size();i++){
		p=ch[p][s[i]-'a'];
		for(j=p;j&&~cnt[j];j=ne[j])
			ans+=cnt[j],cnt[j]=-1;
	}return ans;
}signed main(){
	init();int i;
	string s;
	cin>>n;
	for(i=1;i<=n;i++)
		cin>>s,insert(s);
	build();
	cin>>s;
	cout<<query(s);
	return 0;
}

P5357 【模板】AC 自动机

容易发现,将所有 \(fail_i\)\(i\) 连条边,最后形成了一棵树,我们将其叫做 fail 树。

  • fail 树是一棵有根树,结点 \(i\) 表示的状态为所有 \(j\in subtree(i)\) 状态的后缀。

于是这里我们就建出 fail 树然后统计即可。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5,M=2e6+5;
int n,to[N];
int ch[N][26],ne[N],tot;
inline int insert(string s){
	int p=0,len=(int)s.size();
	for(int i=0;i<len;++i){
		int j=s[i]-'a';
		if(!ch[p][j])ch[p][j]=++tot;
		p=ch[p][j];
	}return p;
}
inline void build(){
	queue<int>q;
	for(int i=0;i<26;++i)
		if(ch[0][i])
			ne[ch[0][i]]=0,q.push(ch[0][i]);
	while(q.size()){
		int u=q.front();q.pop();
		for(int i=0;i<26;++i){
			int v=ch[u][i],w=ch[ne[u]][i];
			if(v)ne[v]=w,q.push(v);
			else ch[u][i]=w;
		}
	}
}
int siz[N],head[N],num;
struct edge{int v,nxt;}e[N];
inline void add(int u,int v){
	e[++num]=(edge){v,head[u]},head[u]=num;
}
void dfs(int u){
	for(int i=head[u];i;i=e[i].nxt)
		dfs(e[i].v),siz[u]+=siz[e[i].v];
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n;string s;
	for(int i=1;i<=n;++i)
		cin>>s,to[i]=insert(s);
	build();cin>>s;
	for(int i=0,p=0;i<s.size();++i)
		p=ch[p][s[i]-'a'],++siz[p];
	for(int i=1;i<=tot;++i)
		add(ne[i],i);
	dfs(0);
	for(int i=1;i<=n;++i)
		cout<<siz[to[i]]<<'\n';
	return 0;
}

P3796 AC 自动机(简单版 II)

和刚才的一模一样,只是统计次数最多的。

P2292 [HNOI2004] L 语言

显然的 DP 是 \(f_i\) 表示 \(s[1,i]\) 是否能够被理解,\(f_i=1\) 的条件为 \(\exist j<i,f_j=1,s[j+1,i]\in D\)。建出自动机,将 \(t\) 在自动机上跑,然后不断跳 \(fail\) 检验,复杂度 \(O(n|s||t|)\),过不去。

发现有 \(|s|\le 20\),我们可以直接预处理每个位置 \(fail\) 能指向哪些长度的状态,这是可以轻松处理的,然后我们就能做到 \(O(n|t|)\)

#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5;
int n,m;
int ch[N][26],ne[N],t[N],tot;
inline void insert(string s){
	int p=0,len=s.size();
	for(int i=len-1;i>=0;--i){
		int j=s[i]-'a';
		if(!ch[p][j])ch[p][j]=++tot;
		p=ch[p][j];
	}t[p]|=1<<len-1;
}
int q[N],l=1,r;
inline void build(){
	for(int i=0;i<26;++i)
		if(ch[0][i])
			q[++r]=ch[0][i];
	while(l<=r){
		int u=q[l++];
		for(int i=0;i<26;++i){
			int v=ch[u][i],w=ch[ne[u]][i];
			if(v)ne[v]=w,q[++r]=v;
			else ch[u][i]=w;
		}
	}for(int i=1;i<=r;++i)
		t[q[i]]|=t[ne[q[i]]];
}
int f[N],g[N];
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	string s;cin>>n>>m;
	for(int i=1;i<=n;++i)
		cin>>s,insert(s);
	build();
	while(m--){
		cin>>s;n=s.size();s=" "+s;f[0]=1;
		for(int i=n,p=0;i;--i)
			g[i]=t[p=ch[p][s[i]-'a']];
		for(int i=1;i<=n;++i)
			f[i]=f[i-1]>>1|(f[i-1]&1?g[i]:0);
		for(int i=n;i>=0;--i)
			if(f[i]&1){cout<<i<<'\n';break;}
	}
	return 0;
}

P2414 [NOI2011] 阿狸的打字机

按照题目的方法建出 AC 自动机,将 P 的位置存一下。

对于询问,如果暴力,是对于 trie 树上 \(y\) 到根的每个点暴力在 fail 树上跳,看是否会到 \(x\)。相当于我们要查询 \(x\) 子树中有多少 \(y\) 在 trie 树上到 根的点,然后离线,遇到路径加上 \(1\),遇到 \(y\) 回溯的时候 \(-1\),到点查询即可。

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=2e5+5,M=2e6+5;
int n,q,to[N],c[N];
int ne[N],fa[N],tot;
int dfn[N],nfd[N],cnt;
int qx[N],qy[N],ans[N];
int ch[N][26];
vector<int> e[N],g[N];
void add(int x,int y){
	while(x<N)c[x]+=y,x+=x&-x;
}int ask(int x){
	int k=0;while(x)k+=c[x],x-=x&-x;return k;
}void build(){
	queue<int>q;
	for(int i=0;i<26;++i)
		if(ch[0][i])
			ne[ch[0][i]]=0,q.push(ch[0][i]);
	while(q.size()){
		int u=q.front();q.pop();
		for(int i=0;i<26;++i){
			int v=ch[u][i],w=ch[ne[u]][i];
			if(v)ne[v]=w,q.push(v);
			else ch[u][i]=w;
		}
	}for(int i=1;i<=tot;++i)
		e[ne[i]].pb(i);
}void dfs(int u){
	dfn[u]=++cnt;
	for(int v:e[u])
		dfs(v);
	nfd[u]=cnt;	
}signed main(){
	freopen("P2414_1.in","r",stdin);
	freopen("gty.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	string s;cin>>s;
	for(int i=0,p=0;i<s.size();++i){
		if(s[i]=='P')to[++n]=p;
		else if(s[i]=='B')p=fa[p];
		else{
			if(!ch[p][s[i]-'a'])
				ch[p][s[i]-'a']=++tot,fa[tot]=p;
			p=ch[p][s[i]-'a'];
		}
	}build();dfs(0);cin>>q;
	for(int i=1;i<=q;++i)
		cin>>qx[i]>>qy[i],g[qy[i]].pb(i);
	for(int i=0,p=0,j=0;i<s.size();++i){
		if(s[i]=='P'){
			++j;
			for(int v:g[j])
				ans[v]=ask(nfd[to[qx[v]]])-ask(dfn[to[qx[v]]]-1);
		}else if(s[i]=='B')add(dfn[p],-1),p=fa[p];
		else p=ch[p][s[i]-'a'],add(dfn[p],1);
	}for(int i=1;i<=q;++i)
		cout<<ans[i]<<'\n';
	return 0;
}

P7456 [CERC2018] The ABCD Murderer

把 AC 自动机建好后,我们把 \(s\) 在自动机上跑,计算 \(dp_i\) 表示拼好 \(s[1,i]\) 至少需要多少单词。由于可以重叠(不可以重叠就没法做了),我们肯定选择最长的,所以我们要查询一段区间内 \(dp\) 最小值,单调队列或线段树都可以。

#include<bits/stdc++.h>
#define mid (l+r>>1)
using namespace std;
const int N=3e5+5,INF=1e9;
void Max(int &x,int y){
	if(y>x)x=y;
}void Min(int &x,int y){
	if(y<x)x=y;
}struct SEG{
	int s[N<<2];
	void build(int p,int l,int r){
		s[p]=INF;if(l==r)return;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
	}void update(int p,int l,int r,int x,int y){
		if(l==r)return s[p]=y,void();
		if(x<=mid)update(p<<1,l,mid,x,y);
		else update(p<<1|1,mid+1,r,x,y);
		s[p]=min(s[p<<1],s[p<<1|1]);
	}int query(int p,int l,int r,int L,int R){
		if(L<=l&&r<=R)return s[p];int res=INF;
		if(L<=mid)Min(res,query(p<<1,l,mid,L,R));
		if(mid<R)Min(res,query(p<<1|1,mid+1,r,L,R));
		return res;
	}
}seg;
int n,m,dp[N];
int ch[N][26],cnt[N],ne[N],tot;
void insert(string s){
	int p=0;
	for(int i=0;i<s.size();++i){
		if(!ch[p][s[i]-'a'])
			ch[p][s[i]-'a']=++tot;
		p=ch[p][s[i]-'a'];
	}Max(cnt[p],s.size());
}void build(){
	queue<int> q;
	for(int i=0;i<26;++i)
		if(ch[0][i])q.push(ch[0][i]);
	while(q.size()){
		int u=q.front();q.pop();
		for(int i=0;i<26;++i){
			int v=ch[u][i],w=ch[ne[u]][i];
			if(v)ne[v]=w,q.push(v);
			else ch[u][i]=w;
		}Max(cnt[u],cnt[ne[u]]);
	}
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	string s,t;cin>>n>>s;
	m=s.size(),s=' '+s;
	for(int i=1;i<=n;++i)
		cin>>t,insert(t);
	memset(dp,0x3f,sizeof(dp));
	build();
	seg.build(1,0,m);
	seg.update(1,0,m,0,0);
	for(int i=1,j=0;i<=m;++i){
		j=ch[j][s[i]-'a'];
		if(i-cnt[j]>i-1)continue;
		else if(i-cnt[j]<1)dp[i]=1;
		else dp[i]=seg.query(1,0,m,i-cnt[j],i-1)+1;
		seg.update(1,0,m,i,dp[i]);
	}cout<<(dp[m]<=m?dp[m]:-1)<<'\n';
	return 0;	
}

P3121 [USACO15FEB] Censoring G

把自动机建出来,按位跑,用栈维护积累的字符串以及在自动机的位置,如果遇到插入的某个串尾部,向后弹栈,更新在自动机的位置就行。

#include<bits/stdc++.h>
#define mid (l+r>>1)
using namespace std;
const int N=2e5+5,INF=1e9;
int n,m;
stack<char> st;
stack<int> gty;
int ch[N][26],cnt[N],ne[N],tot;
void insert(string s){
	int p=0;
	for(int i=0;i<s.size();++i){
		if(!ch[p][s[i]-'a'])
			ch[p][s[i]-'a']=++tot;
		p=ch[p][s[i]-'a'];
	}cnt[p]=s.size();
}void build(){
	queue<int> q;
	for(int i=0;i<26;++i)
		if(ch[0][i])q.push(ch[0][i]);
	while(q.size()){
		int u=q.front();q.pop();
		for(int i=0;i<26;++i){
			int v=ch[u][i],w=ch[ne[u]][i];
			if(v)ne[v]=w,q.push(v);
			else ch[u][i]=w;
		}
	}
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	string s,t;cin>>s>>n;
	m=s.size(),s=' '+s;
	for(int i=1;i<=n;++i)
		cin>>t,insert(t);
	build();
	gty.push(0);
	for(int i=1,p=0;i<=m;++i){
		st.push(s[i]);
		p=ch[p][s[i]-'a'];
		gty.push(p);
		if(cnt[p]){
			for(int j=0;j<cnt[p];++j)
				st.pop(),gty.pop();
			p=gty.top();
		}
	}string ans;
	while(st.size())ans=ans+st.top(),st.pop();
	reverse(ans.begin(),ans.end());
	cout<<ans<<'\n';
	return 0;	
}

P4052 [JSOI2007] 文本生成器

容斥一下,转化成求一个都不出现的个数。

建出自动机,并处理每个点一直按 \(fail\) 走是否能走到某个串尾。\(dp_{i,j}\) 表示长度为 \(i\),此时在自动机的节点 \(j\) 上,枚举 \(j\) 的走向,判断是否合法,添加即可。

#include <bits/stdc++.h>
using namespace std;
const int N=6005,M=105,P=10007;
int n,m,ans=1,dp[M][N];
int ch[N][26],cnt[N],ne[N],tot;
void insert(string s){
	int p=0;
	for(int i=0;i<s.size();++i){
		if(!ch[p][s[i]-'A'])
			ch[p][s[i]-'A']=++tot;
		p=ch[p][s[i]-'A'];
	}cnt[p]=1;
}void build(){
	queue<int>q;
	for(int i=0;i<26;++i)
		if(ch[0][i])
			q.push(ch[0][i]);
	while(q.size()){
		int u=q.front();q.pop();
		for(int i=0;i<26;++i){
			int v=ch[u][i],w=ch[ne[u]][i];
			v?q.push(v),ne[v]=w:ch[u][i]=w;
		}cnt[u]|=cnt[ne[u]];
	}
}signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i){
		string s;
		cin>>s,insert(s);
	}build();
	dp[0][0]=1;
	for(int i=1;i<=m;++i){
		for(int j=0;j<=tot;++j){
			for(int k=0;k<26;++k){
				if(!cnt[ch[j][k]])
					(dp[i][ch[j][k]]+=dp[i-1][j])%=P;
			}
		}ans=ans*26%P;
	}for(int i=0;i<=tot;++i)
		ans=(ans-dp[m][i]+P)%P;
	cout<<ans<<'\n';
	return 0;
}

P3735 [HAOI2017] 字符串

两个字符串 \(s,t\) “相等”等价于 \(lcp+lcp+k>|s|\)。然后就可以得到 \(60\) pts。

将所有 \(p_i\) 的正反两串都丢进一个 tire,建出 \(fail\) 树。考虑 \(lcp\) 走到第 \(i\) 位,那么 \(i+k+1\sim |p_i|\) 都要匹上 \(lcs\)

假设 \(s_1\to s_i\) 走到结点 \(x\)\(s_{|s|}\to s_{i+k+1}\) 走到结点 \(y\)\(p_{1}\to p_{j}\) 走到到结点 \(u\)\(p_{|p|}\to p_{j+k+1}\) 走到结点 \(v\)

当且仅当 \(x\)\(u\) 子孙,\(y\)\(v\) 子孙,此时对 \(p\) 有贡献。

问题是我们这样算的是 \(\le k\) 的答案,要计算恰好为 \(k\) 的将 \(\le k-1\) 的减掉就行。

#include<bits/stdc++.h>
#define eb emplace_back
using namespace std;
const int N=4e5+5;
char S[N],s[N];
int n,m,k,Q,ans[N],tmp[N];
int ch[N][94],ne[N],tot;
int gty[N],zyq[N],cnt;
vector<int> e[N];
struct node{int id,p,q;};
vector<node> v1[N],v2[N];
struct BIT{
    int c[N];
    void add(int x){while(x<=tot+2)++c[x],x+=x&-x;}
    int ask(int x){int k=0;while(x)k+=c[x],x-=x&-x;return k;}
    int ask(int l,int r){return ask(r)-ask(l-1);}
}c1,c2;
void build(){
    queue<int>q;
    for(int i=0;i<=93;++i)
        if(ch[0][i])
            ne[ch[0][i]]=0,q.push(ch[0][i]);
    while(q.size()){
        int u=q.front();q.pop();
        for(int i=0;i<=93;++i){
            int v=ch[u][i],w=ch[ne[u]][i];
            if(v)ne[v]=w,q.push(v);
            else ch[u][i]=w;
        }
    }for(int i=1;i<=tot;++i)
        e[ne[i]].eb(i);
    tmp[n+1]=0;
    for(int i=n,p=0;i>=1;--i)
        tmp[i]=p=ch[p][S[i]-33];
    for(int i=0,p=0;i<=n-k;++i)
        v2[p].eb((node){0,tmp[i+k+1],tmp[i+k]}),
        p=ch[p][S[i+1]-33];
}void dfs1(int u){
    gty[u]=++cnt;
    for(int v:e[u])dfs1(v);
    zyq[u]=cnt;
}void dfs2(int u){
    for(node t:v1[u]){
        ans[t.id]-=c1.ask(gty[t.p],zyq[t.p]);
        if(~t.q)ans[t.id]+=c2.ask(gty[t.q],zyq[t.q]);
    }for(node t:v2[u])c1.add(gty[t.p]),c2.add(gty[t.q]);
    for(int v:e[u])dfs2(v);
    for(node t:v1[u]){
        ans[t.id]+=c1.ask(gty[t.p],zyq[t.p]);
        if(~t.q)ans[t.id]-=c2.ask(gty[t.q],zyq[t.q]);
    }
}signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>k>>S+1>>Q,n=strlen(S+1);
    for(int t=1;t<=Q;++t){
        cin>>s+1,m=strlen(s+1);
        if(m<=k){ans[t]=n-m+1;continue;}
        for(int i=1,p=0;i<=m;++i){
            if(!ch[p][s[i]-33])
                ch[p][s[i]-33]=++tot;
            p=ch[p][s[i]-33];
        }for(int i=m,p=0;i>=1;--i){
            if(!ch[p][s[i]-33])
                ch[p][s[i]-33]=++tot;
            tmp[i]=p=ch[p][s[i]-33];
        }tmp[m+1]=0;
        for(int i=0,p=0;i<=m-k;++i){
            if(i)v1[p].eb((node){t,tmp[i+k+1],tmp[i+k]});
            else v1[p].eb((node){t,tmp[i+k+1],-1});
            p=ch[p][s[i+1]-33];
        }for(int i=1;i<=m;++i)
            s[i]=0;
    }build(),dfs1(0),dfs2(0);
    for(int i=1;i<=Q;++i)
        cout<<ans[i]<<'\n';
    return 0;
}
posted @ 2025-08-19 21:09  zzy0618  阅读(9)  评论(0)    收藏  举报