前置知识
- \(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
WYWYWYWYWYWYWYWY
如果是 WYWYWYWYW
的形式,找出来一定是这样。
WYWYWYWYW
WYWYWYWWYWYWYW
因此 \(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)\),则。
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);elsech[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;
}