来自\(\texttt{SharpnessV}\)的省选复习计划中的字符串。
字符串哈希,一般 \(\rm H(S)=\sum\limits_{i=1}^{Len}bas^{Len-i}\times S_i \bmod P\)。这样我们对一个字符串预处理出它的前缀哈希值和 \(\rm bas\) 的次幂,可以做到 \(\rm O(1)\) 求子串的哈希值。
众所周知哈希可以针对模数 \(P\) 进行 \(\rm Hack\),所以我们一般采用双哈希,即两个不同的模数。
常用哈希模数:\(998244353\ ,\ 1000000007\ ,\ 1000000009\ ,\ 998244853\ ,\ 1004535809\ ,\ 469762049\)。
常用\(\rm bas\):\(131\ ,\ 127\ ,\ 137\ ,\ 151\)。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define P 998244853
#define Q 998244353
#define N 10005
#define bas 131
using namespace std;
int n;pair<int,int>a[N];char s[N];
int main(){
scanf("%d",&n);
rep(i,1,n){
scanf("%s",s+1);
int X=0,Y=0,len=strlen(s+1);
rep(i,1,len)X=(1LL*X*bas+s[i])%P,Y=(1LL*Y*bas+s[i])%Q;
a[i]=make_pair(X,Y);
}
sort(a+1,a+n+1);
int ans=0;rep(i,1,n)ans+=a[i]!=a[i-1];
printf("%d\n",ans);
return 0;
}
这里我们运用字符串哈希做到 \(\rm O(1)\) 判断两个子串是否相等,然后用树状数组维护前缀和算贡献。
本题出题人没有刻意卡,自然溢出也能通过。时间复杂度\(\rm O(N\ln N)\)。
#include<bits/stdc++.h>
#define rep(i,a,b) for(register int i=a;i<=b;i++)
#define pre(i,a,b) for(register int i=a;i>=b;i--)
#define N 1050005
#define bas 131
using namespace std;
int c[27],n,d[27],nxt[N];char s[N];unsigned pw[N],h[N];
inline void add(int x){x++;for(;x<=27;x+=x&-x)c[x]++;}
inline int ask(int x){x++;int sum=0;for(;x;x-=x&-x)sum+=c[x];return sum;}
inline void solve(){
scanf("%s",s+1);n=strlen(s+1);
rep(i,1,n)h[i]=h[i-1]*bas+s[i];
memset(c,0,sizeof(c));int tot=0;long long ans=0;
memset(d,0,sizeof(d));
pre(i,n,1){
if(d[s[i]-'a']^=1)tot++;else tot--;
nxt[i]=tot;
}
memset(d,0,sizeof(d));
d[s[1]-'a']^=(tot=1);add(1);
rep(i,2,n-1){
unsigned cur = h[i];
int w[2];
w[1]=ask(nxt[i+1]);
if(i+i<n)w[0]=ask(nxt[i+i+1]);
rep(j,1,n){
if(i*j>=n)break;
if(h[i*j]-pw[i]*h[i*j-i]!=cur)break;
ans+=w[j&1];
}
if(d[s[i]-'a']^=1)tot++;else tot--;
add(tot);
}
printf("%lld\n",ans);
}
int main(){
pw[0]=1;rep(i,1,N-5)pw[i]=bas*pw[i-1];
int T;scanf("%d",&T);
while(T--)solve();
return 0;
}
这里的字符串只要相同字符出现次数相等即为相等。
我们重新设计哈希函数\(\rm H(S)=\sum\limits_{i=1}^{Len}bas^{S_i}\)。
这里的\(\rm bas\)一定要大于字符集,本题取\(1000037\)。然后用双端队列维护一下即可。
\(\rm KMP\)算法的核心是失配数组 \(\rm nxt\) 。\(\rm nxt_i\)表示整个串的前缀与以 \(i\) 结尾的后缀的最长的真匹配长度。
#include<bits/stdc++.h>
using namespace std;
char a[1000005],b[1000005];
int n,m,nxt[1000005];
int main()
{
scanf("%s%s",a+1,b+1);
n=strlen(a+1);m=strlen(b+1);
nxt[1]=0;
for(int i=2,j=0;i<=m;i++){
while(j&&b[j+1]!=b[i])j=nxt[j];
if(b[j+1]==b[i])j++;
nxt[i]=j;
}
for(int i=1,j=0;i<=n;i++){
while(j&&a[i]!=b[j+1])j=nxt[j];
if(a[i]==b[j+1])j++;
if(j==m)printf("%d\n",i-m+1);
}
for(int i=1;i<=m;i++)printf("%d ",nxt[i]);
putchar('\n');return 0;
}
先求出 \(\rm nxt\) 数组,然后再模拟一遍 \(\rm KMP\) 过程,不过现在\(2\times j> i\)就是失配状态,需要跳指针。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define N 1000005
#define P 1000000007
using namespace std;
char s[N];
int num[N],nxt[N],n;
void work(){
scanf("%s",s+1);
n=strlen(s+1);
num[1]=1;nxt[1]=0;
int j=0;
rep(i,2,n){
while(j&&s[j+1]!=s[i])j=nxt[j];
if(s[j+1]==s[i])j++;
nxt[i]=j;num[i]=num[j]+1;
}
j=0;long long ans=1;
rep(i,2,n){
while(j&&s[j+1]!=s[i])j=nxt[j];
if(s[i]==s[j+1])j++;
while(j*2>i)j=nxt[j];
ans=(ans*(long long)(num[j]+1))%P;
}
printf("%lld\n",ans);
}
int main(){
int T;scanf("%d",&T);
while(T--)work();
return 0;
}
\(\rm KMP\) 上跑 \(\rm DP\)。
定义 \(f[i]\) 表示覆盖前缀 \(i\) 的最小长度。
如果\(f[i]\le \rm 2\times nxt[i]\),则\(f[i]=f[\rm nxt[i]]\)。因为我们先覆盖前\(\rm nxt[i]\)个,然后覆盖后 \(\rm nxt[i]\)个。
如果存在 \(f[j]=f[\rm nxt[i]]\) 并且 \(j+\rm nxt[i]\ge i\),则\(f[i]=f[nxt[i]]\)。因为我们先覆盖前\(j\)个,然后覆盖后 \(\rm nxt[i]\)个。
否则显然 \(f[i]=i\)。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 500005
using namespace std;
char s[N];int n,nxt[N],f[N],pre[N];
int main(){
scanf("%s",s+1);
n=strlen(s+1);
int j=0;nxt[1]=0;
rep(i,2,n){
while(j&&s[j+1]!=s[i])j=nxt[j];
if(s[j+1]==s[i])j++;
nxt[i]=j;
}
rep(i,1,n){
f[i]=i;
if(nxt[i]&&pre[f[nxt[i]]]>=(i-nxt[i]))f[i]=f[nxt[i]];
pre[f[i]]=i;
}
printf("%d\n",f[n]);
return 0;
}
这里我们定义状态\(f[i][j]\),表示长短串分别匹配了 \(i\) 位和 \(j\) 位的方案数。
观察到 \(N\) 很大,可以矩阵快速幂。
\(\rm manacher\) 算法是优化的暴力。
如果我们枚举中点,然后暴力扩展长度,时间复杂度是\(\rm O(N^2)\)的。
考虑优化,我们记录当前扩展过的最右边界,和扩展出这个边界的中点。不难发现具有对称性。
这样我们就将暴力优化至 \(\rm O(N)\)。
一个技巧是在原串的两个字符之间插入相同的不在原串中的的字符 \(\texttt{0}\),使得所有的回文串都变成奇数长度的。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 22000005
using namespace std;
char s[N],a[N];
int n,m,r[N];
int main(){
scanf("%s",s+1);
m=strlen(s+1);
a[0]='!';a[++n]='#';
rep(i,1,m)a[++n]=s[i],a[++n]='#';
int mx=0,mid=0;
rep(i,1,n){
if(i>mx){
r[i]=1;
while(a[i+r[i]]==a[i-r[i]])r[i]++;
mx=i+r[i]-1,mid=i;
}
else{
r[i]=min(r[(mid<<1)-i],mx-i+1);
while(a[i+r[i]]==a[i-r[i]])r[i]++;
if(i+r[i]-1>mx)mx=i+r[i]-1,mid=i;
}
}
int ans=0;
rep(i,1,n)ans=max(ans,r[i]-1);
printf("%d\n",ans);
return 0;
}
在\(\rm manacher\)的同时预处理出以 \(i\) 为左/右边界的最长回文串。然后枚举断点得到最长双回文串。
P4683 [IOI2008] Type Printer 打印机
观察一下打印机的基本操作就是在\(\rm Trie\)树上移动的过程。
我们先建立出\(\rm Trie\)树,然后从根出发,遍历所有点后,在深度最深的节点结束。
不难设计\(\rm DP\)方程 \(f[i]\) 表示前缀 \(i\) 是否能够被解读。
然后我们借助\(\rm Trie\)树可以快速判断可以向后扩展多少。
加强后可以用\(\rm AC\)自动机解决。
我们可以在\(\rm Trie\)树中插入通配符,完成字符串的模糊匹配。
转换一下模型:给定 \(N\) 个数,求异或和最大的二元组。
我们可以将每个数拆成二进制,然后从高位到低位插入\(\rm Trie\)树。
插入之前,先查询与当前数的异或最大值。优先考虑高位,所以每一位的选择是唯一的。
\(01\rm Trie\) 可以支持 \(+1\) ,查询所有数异或和,查询与 \(a\) 异或和 \(\le b\) 的数的个数。
\(\rm AC\)自动机是 \(\rm KMP\) 思想在 \(\rm Trie\) 上的延续。
我们可以从\(1\sim i-1\) 的 \(\rm nxt\) 值推出 \(\rm nxt_{i}\)的值,同理我们可以借助\(\rm BFS\),从深度\(<\rm dep_i\)的 \(\rm fail\) 值推出 \(\rm fail_i\) 。
\(\rm AC\)自动机中有用的东西有两样,一个是转移边\(\rm nxt\),一个是失配指针\(\rm fail\)。
\(\rm nxt\) 构成一张图,而 \(\rm fail\) 构成一棵树。图我们可以用来跑\(\rm DP\),树我们可以用来统计贡献。
#include<bits/stdc++.h>
using namespace std;
int n,ch[110005][27],tot;
int fail[110005],ed[110005],v[1600];
char s[1600][1000],t[1000005];
queue<int>q;
void bfs(){
memset(fail,0,sizeof(fail));
for(int i=0;i<26;i++)
if(ch[0][i])q.push(ch[0][i]),fail[ch[0][i]]=0;
while(!q.empty()){
int x=q.front();q.pop();
for(int i=0;i<26;i++)
if(ch[x][i])fail[ch[x][i]]=ch[fail[x]][i],q.push(ch[x][i]);
else ch[x][i]=ch[fail[x]][i];
}
}
int main()
{
//freopen("testdata (9).in","r",stdin);
scanf("%d",&n);
while(n){
memset(ch,0,sizeof(ch));
memset(ed,0,sizeof(ed));
tot=0;
for(int i=1;i<=n;i++){
scanf("%s",s[i]+1);
int l=strlen(s[i]+1),now=0;
for(int j=1;j<=l;j++){
if(!ch[now][s[i][j]-'a'])ch[now][s[i][j]-'a']=++tot;
now=ch[now][s[i][j]-'a'];
}
ed[now]=i;
}
scanf("%s",t+1);
bfs();int l=strlen(t+1),now=0,ans=0;
memset(v,0,sizeof(v));
for(int i=1;i<=l;i++){
now=ch[now][t[i]-'a'];
int x=now;
while(x){
v[ed[x]]++;
x=fail[x];
}
}
int mx=0;
for(int i=1;i<=n;i++)mx=max(mx,v[i]);
printf("%d\n",mx);
for(int i=1;i<=n;i++)if(mx==v[i])puts(s[i]+1);
scanf("%d",&n);
}
return 0;
}
对于每个\(S\)的每个前缀,在自动机对应的节点上打上标记,表示对沿着\(fail\)树一直到根的路径产生贡献。
最后\(\rm DFS\)一下\(\rm fail\)树统计贡献即可。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define N 400005
#define M 2000006
using namespace std;
int n;char s[M];
int ch[N][27],rt,tot,fail[N];
queue<int>q;
int h[N],tt,c[N],ed[N];
struct edge{
int to,nxt;
}e[N<<1];
void add(int x,int y){
e[++tt].nxt=h[x];h[x]=tt;e[tt].to=y;
}
void bfs(){
rep(i,0,25)if(ch[rt][i])fail[ch[rt][i]]=0,q.push(ch[rt][i]);
while(!q.empty()){
int x=q.front();q.pop();
add(fail[x],x);
rep(i,0,25)if(ch[x][i])
fail[ch[x][i]]=ch[fail[x]][i],q.push(ch[x][i]);
else ch[x][i]=ch[fail[x]][i];
}
}
void dfs(int x,int fa){
for(int i=h[x];i;i=e[i].nxt)if(e[i].to!=fa)
dfs(e[i].to,x),c[x]+=c[e[i].to];
}
int main(){
scanf("%d",&n);
rep(i,1,n){
scanf("%s",s+1);
int len=strlen(s+1);
int now=rt;
rep(j,1,len){
if(!ch[now][s[j]-'a'])ch[now][s[j]-'a']=++tot;
now=ch[now][s[j]-'a'];
}
ed[i]=now;
}
bfs();
scanf("%s",s+1);
int len=strlen(s+1);
int now=rt;
rep(i,1,len)now=ch[now][s[i]-'a'],c[now]++;
dfs(0,0);
rep(i,1,n)printf("%d\n",c[ed[i]]);
return 0;
}
题目已经将\(\rm Trie\)树给出了,直接建立自动机即可。
对于一个串,它每个前缀对应的节点,都会在到根的\(fail\)树上产生贡献。
我们先建出\(fail\)树,对询问离线,然后支持单点加和区间查询即可。
先建立自动机。
不难发现每在结尾添加一个字母,等价于在图上走一步。
如果图上有环,说明存在无限长的满足条件的串。
自动机上\(\rm DP\),\(f[i][j]\) 表示长度为 \(i\) 的串,匹配到自动机上的节点 \(j\) 的方案数。
同样是自动机上跑 \(\rm DP\)。
一般采用\(\rm O(N\log N)\)的倍增方法。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 1000005
using namespace std;
char s[N];int x[N],y[N],sa[N],c[N],n,m='z';
int main(){
scanf("%s",s+1);n=strlen(s+1);
rep(i,1,n)c[x[i]=s[i]]++;
rep(i,1,m)c[i]+=c[i-1];
rep(i,1,n)sa[c[x[i]]--]=i;
for(int k=1;k<=n;k<<=1){
int num = 0;
rep(i,n-k+1,n)y[++num]=i;
rep(i,1,n)if(sa[i]>k)y[++num]=sa[i]-k;
rep(i,1,m)c[i]=0;
rep(i,1,n)c[x[i]]++;
rep(i,1,m)c[i]+=c[i-1];
pre(i,n,1)sa[c[x[y[i]]]--]=y[i];
rep(i,1,n)swap(x[i],y[i]);
x[sa[1]]=num=1;
rep(i,2,n)x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]?num:++num);
m=num;if(n==m)break;
}
rep(i,1,n)printf("%d ",sa[i]);
return 0;
}
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 2000005
using namespace std;
struct SAM{
int nxt[N][26],fa[N],len[N],pre,idx,sz[N];
SAM(){fa[0]=~0;}
void extend(int ch){
int cur=++idx,p=pre;len[cur]=len[p]+1;
while(~p&&!nxt[p][ch])nxt[p][ch]=cur,p=fa[p];
if(-1==p)fa[cur]=0;
else{
int q=nxt[p][ch];
if(len[p]+1==len[q])fa[cur]=q;
else{
int now=++idx;len[now]=len[p]+1;
rep(i,0,25)nxt[now][i]=nxt[q][i];
fa[now]=fa[q];fa[q]=fa[cur]=now;
while(~p&&nxt[p][ch]==q)nxt[p][ch]=now,p=fa[p];
}
}sz[pre=cur]++;
}
int b[N],c[N];
void solve(){
long long ans = 0;
rep(i,1,idx)c[len[i]]++;
rep(i,1,idx)c[i]+=c[i-1];
rep(i,1,idx)b[c[len[i]]--]=i;
pre(i,idx,1){
sz[fa[b[i]]]+=sz[b[i]];
if(sz[b[i]]>1)ans=max(ans,1LL*sz[b[i]]*len[b[i]]);
}
printf("%lld\n",ans);
}
}w;
int main(){
register char ch = getchar();
while(ch != '\n')w.extend(ch - 'a') , ch = getchar();
w.solve();
return 0;
}
正解回文自动机。
\(\rm manacher+SAM\) 也可以。
由于\(\rm manacher\) 的对称性,只有最右指针向右扩展的时候,才会产生新的有贡献的子串。
我们再在\(\rm SAM\)中计算这个子串的存在值并更新答案。
然后是喜乐见闻的卡空间情节。
最小表示法,将串复制一倍然后后缀排序。
枚举起点,然后匹配。考虑到最多只有三次失配,所以直接暴力求\(\rm LCP\)即可。
一个串的不同子串个数是对应后缀自动机的 \(\sum Len_i\) 。
加入一个节点的时候,发现除了新加的节点,其他节点的\(\sum Len_i\) 不变,所以我们只用在答案中加入新加节点的 \(Len_i -Len_{fa_i}\)。
还有一种动态维护\(\rm SA\)的做法,思维难度高一点,这里不再赘述。
不同于\(\rm AC\)自动机,\(\rm SAM\)的转移边构成一个有向无环图,一条从源点出发的路径对应一个子串。
我们可以一遍拓扑求出从当前点开始的路径条数,然后在图上求出第\(k\)小的路径。
注意不同位置的相同子串不算一次,我可以利用\(\rm endpos\)的\(\rm size\)计算。
求所有后缀之间的两两\(\rm LCP\)之和。
不难发现这就是后缀树上两两\(\rm LCA\)的深度之和,反串跑\(\rm SAM\)即可。
也可以用\(\rm SA\)解决,我们先求出后缀数组和 \(\rm Height\) 数组,然后利用单调栈维护集合。
用一个分隔符,然后将两个串连起来求\(\rm SA\)。
然后用单调栈分别计算 \(A\) 对 \(B\) 的贡献和 \(B\) 对 \(A\) 的贡献。
这道题的关键点操作非常值得一做。
我们枚举长度\(Len\),然后每隔\(Len\)设置一个关键点,如果存在\(AA\)使得\(A\)长度大于等于\(Len\),一定过两个关键点。
我们只用在关键点之间求\(\rm LCP/LCS\)即可,后缀数组即可。
如果我们将 \(\rm Height\ge r\) 的两个后缀合并,那么我们降序枚举 \(r\) ,联通块个数将会越来越少。
我们对每个联通块维护最大值最小值和大小即可,只需要并查集。
P4081 [USACO17DEC]Standing Out from the Herd P
广义后缀自动机。
可以在插入一个串之后将 \(\rm pre\) 指针重置为 \(0\),这样除了会多出一些空节点,对时间复杂度和正确性没有影响。
不过最好的方式是先建\(\rm Trie\)树,然后记录\(\rm pre_i\),表示第\(i\)个后缀对应的节点,\(\rm BFS\)的时候以\(\rm Trie\)树上父亲的\(pre\)为前指针即可。
比较毒瘤的\(\rm SAM\)。
我们先对喵的姓和名一起建立\(\rm Parent\)树,然一次点名对应一个子树的数颜色。大力莫队即可。
第二问一只喵被点了多少次,我们可以在莫队的时候将颜色弹出的时候将产生贡献的区间加一下即可。
数据范围较小,卡常还好。
非常简单的一道题。
我们先将\(\rm Parent\)树建立出来,那么插入一个串就是路径加,然后查询树上权值\(\ge k\)的点个数。
大力树剖可以做到\(\rm O(N\log^2N)\)。
如果离线一下,考虑每个点的子树中,第\(k\)次插入的时间戳,这就是经典问题子树第\(k\)小,线段树合并一下即可。
经典模型,维护\(\rm SAM\)的\(\rm endpos\)集合。
众所周知集合大小之和是\(\rm O(N^2)\)的。
所以我们借助线段树合并,可以在\(\rm O(N\log N)\)的时间内求出每个节点的\(\rm endpos\)集合并解决问题。
这类模型比较肝是真的。
重工业。
思维难度不大,首先我们需要建立\(\rm Parent\)树,然后对于给定串,拆出他对应的点,并建图。
先跑一遍强连通分量判断是否有解,然后一遍拓扑求出最长路。
同样用线段树合并维护\(\rm endpos\)集合,然后直接集合中查询即可。
同理维护\(\rm endpos\),较前几题简单一些。