前缀函数,AC自动机
洛谷p2375
前缀函数的应用。
题目大意:我们都知道前缀函数,但现在要求求出一个更强大num
数组一一对于字符串S的前i个字符构成的子串,既是它的后缀同时又是它的前缀,并且该后缀与该前缀不重叠,将这种字符串的数量记作num[i]。
其实只要找出最长符合要求前缀即可,更短的一定是最长符合要求前缀的前缀,推前缀函数时求一个“前缀”和就好(即累加到最长前缀上)。如何求最长不重合既是前缀又是后缀的串呢?前缀函数求的会包含重合的,想一想最开始重合那个过程,在i-1的位置如i=7,在i=6的位置p[i]一定等于3,这样i=7时才可能重合,重合时我们只需令 j = p [ j − 1 ] j=p[j-1] j=p[j−1]即可。其他过程和求前缀函数一样,注意j设在外面,每次只用从上一次匹配的位置匹配,这样每次重合也最多重合一个字符,跳一会p[j-1]即可.
(啊,写出来好不清晰,如果掌握了前缀函数,这个还是可以想到的,至少最直观可以想到一直跳p[j-1]至不重合,用倍增处理可以降低时间复杂度,然而还是80,看题解有用倍增过的,还只是改了数组一维和二维的位置,好神奇。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
const int mod=1e9+7;
typedef long long ll;
int p[maxn],z[maxn],np[maxn],num[maxn],f[maxn][21];
char s[maxn];
void getpi(int n)
{
p[0]=0,num[0]=0;
for(int i=1;i<n;i++)
{
int j=p[i-1];
while(j&&s[j]!=s[i]) j=p[j-1];
if(s[i]==s[j]) j++;
p[i]=j;
if(j) num[i]=1+num[j-1];
else num[i]=0;
}
}
void pretreat(int n)
{
for(int i=0;i<n;i++) f[i][0]=p[i];
int l=0;
while((1<<(l+1))<=n) l++;
for(int i=1;i<=l;i++)
{
for(int j=1;j<n;j++)
{
if(f[j][i-1]-1)
f[j][i]=f[f[j][i-1]-1][i-1];
else f[j][i]=0;
}
}
}
ll solve(int n)
{
np[0]=0;
/*int l=0;
while((1<<(l+1))<=n) l++;*/
int j=0;
for(int i=1;i<n;i++)
{
while(j&&s[j]!=s[i]) j=p[j-1];
if(s[j]==s[i]) j++;
if(2*j>i+1) j=p[j-1];
/*int j=i;
for(int k=l;k>=0;k--)
{
if(f[j][k]*2>i+1)
{
j=f[j][k]-1;
}
}
if(j)
j=f[j][0];*/
if(j)
np[i]=1+num[j-1];
else np[i]=0;
}
ll ans=1;
for(int i=0;i<n;i++)
{
ans*=(np[i]+1);
ans%=mod;
}
return ans;
}
int main()
{
// freopen("tte.txt","r",stdin);
int n;scanf("%d",&n);
while(n--)
{
scanf("%s",s);
int l=strlen(s);
getpi(l);
//pretreat(l);
ll ans=solve(l);
printf("%lld\n",ans);
}
return 0;
}
牛客2020多校第二场A
写了一遍忘保存了!
利用前缀函数来容斥。要点:字符串哈希,把字符串所有后缀用map储存并对应好个数,那么枚举所有字符串所有前缀,计算对应个数,对应该前缀其长度为其前缀函数的前缀也统计过,但此时他们已不是最长,减去对应个数。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int mod1=998244353;
//const ll mod2=1222827239;
const int x=2333;
const int maxn=1e5+5;
unordered_map<ull,int> op;
int id(char c)
{
return c-'a'+1;
}
ull hx[maxn*10];
ull tx[maxn*10];
void getsufhx(char* s)
{
int l=strlen(s);
ull o=0;
for(int i=0;i<l;i++)
{
o=o*x+id(s[i]);
hx[i]=o;
}
for(int i=0;i<l;i++)
{
ll u=hx[l-1]-(i==0?0:hx[i-1])*tx[l-i];
// cout<<u<<endl;
op[u]+=1;
}
}
char s[maxn*25];
int sta[maxn],p[maxn*20];
int main()
{
//freopen("tte.txt","r",stdin);
int n;scanf("%d",&n);
int st=0;
tx[0]=1;
for(int i=1;i<=1000001;i++)
{
tx[i]=tx[i-1]*x;
}
for(int i=1;i<=n;i++)
{
sta[i]=st;
scanf("%s",s+st);
getsufhx(s+st);
int l=strlen(s+st);
char* ss=s+st;
p[st]=0;
for(int j=1;j<l;j++)
{
int k=p[st+j-1];
while(k&&ss[k]!=ss[j]) k=p[st+k-1];
if(ss[k]==ss[j]) k+=1;
p[st+j]=k;
}
st+=l+1;
}
ll ans=0;
for(int i=1;i<=n;i++)
{
int l=strlen(s+sta[i]);
char* ss=s+sta[i];
ull tmp=0;
for(int j=0;j<l;j++)
{
tmp=tmp*x+id(ss[j]);
// cout<<tmp<<endl;
ans+=op[tmp]*1ll*(j+1)%mod1*(j+1)%mod1;
ans-=op[tmp]*1ll*p[sta[i]+j]%mod1*p[sta[i]+j]%mod1;
ans=ans%mod1+mod1;
ans%=mod1;
}
}
printf("%lld\n",ans);
return 0;
}
HDU4758
题目大意:求一个长度为n+m的序列,只包含n个R,m个D两个字母,且给出两个R,D序列S,T,所求串必须包含S和T.
求这样的序列数。
AC自动机上的dp问题,同时还要表示已经出现了哪个要求子串,因为只有两个用两位二进制位即可表示所有状态,同时还需要记录已用几个R,几个D,在AC自动机哪个位置。之后逐一枚举即可,要先枚举状态,因为大的状态必然由小的状态推得。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=1e9+7;
const int maxn=200+5;
const int sigma_size=2;
int ch[maxn][sigma_size],val[maxn],sz,m,n;
int idx(char c)
{
return c!='D';
}
int d[maxn][105][105][4];
void init()
{
sz=1;
memset(ch[0],0,sizeof(ch[0]));
val[1]=0;
// memset(vis,0,sizeof(vis));
}
void insert(char *s,int v)
{
int u=0,len=strlen(s);
for(int i=0;i<len;i++)
{
int c=idx(s[i]);
if(!ch[u][c])
{
memset(ch[sz],0,sizeof(ch[sz]));
val[sz]=0;
ch[u][c]=sz++;
}
u=ch[u][c];
}
val[u]=v;
}
int f[maxn],last[maxn];
queue<int> q;
void getfail()
{
f[0]=0;
for(int c=0;c<sigma_size;c++)
{
int u=ch[0][c];
if(u)
{
f[u]=0;
q.push(u);
last[u]=0;
}
}
while(!q.empty())
{
int r=q.front();q.pop();
for(int c=0;c<sigma_size;c++)
{
int u=ch[r][c];
if(!u)
{
ch[r][c]=ch[f[r]][c];
continue;
}
q.push(u);
int v=f[r];
while(v&&!ch[v][c]) v=f[v];
f[u]=ch[v][c];
val[u]|=val[f[u]];
last[u]=val[f[u]]?f[u]:last[f[u]];
}
}
}
char s[105],t[105];
int main()
{
//freopen("tte.txt","r",stdin);
int T;scanf("%d",&T);
while(T--)
{
init();
scanf("%d%d",&m,&n);
scanf("%s%s",s,t);
insert(s,1);
insert(t,2);
getfail();
//int ans=solve(0,0,0,0);
memset(d,0,sizeof(d));
int ans=0;
d[0][0][0][0]=1;
for(int st=0;st<4;st++)
{
for(int k=0;k<=n;k++)
{
for(int l=0;l<=m;l++)
{
for(int i=0;i<sz;i++)
{
if(d[i][k][l][st])
{
if(k<n)
d[ch[i][0]][k+1][l][st|val[ch[i][0]]]=(d[ch[i][0]][k+1][l][st|val[ch[i][0]]]+d[i][k][l][st])%mod;
if(l<m)
d[ch[i][1]][k][l+1][st|val[ch[i][1]]]=(d[ch[i][1]][k][l+1][st|val[ch[i][1]]]+d[i][k][l][st])%mod;
}
}
}
}
}
for(int i=0;i<sz;i++)
{
ans+=d[i][n][m][3];
ans%=mod;
}
printf("%d\n",ans);
}
return 0;
}
CSP20次认证第五题
题目大意:csp题又臭又长。
这题也是AC自动机上的dp,两个要求密文中不能出现字典中词:即不能经过AC自动机中单词节点且沿fail边到根的所有节点都不是单词节点。这题要记录长度,在日记本哪页,在AC自动机什么位置。之后还要枚举添加哪个串,注意给的是密文到明文,可以在输入时直接转换位明文到密文。先枚举长度,长的由短的推得,其他的随意。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=998244353;
const int maxn=100+5;
const int sigma_size=26;
int ch[maxn][sigma_size],val[maxn],sz;
int mp[maxn][26],nxt[maxn][26];
int n,m;
char s[maxn][maxn];
int idx(char c)
{
return c-'a';
}
void init()//啊啊啊,一定要调用一下
{
sz=1;
memset(ch[0],0,sizeof(ch[0]));
memset(val,0,sizeof(val));
}
void insert(char *s,int v)
{
int u=0,len=strlen(s);
for(int i=0;i<len;i++)
{
int c=idx(s[i]);
if(!ch[u][c])
{
memset(ch[sz],0,sizeof(ch[sz]));
val[sz]=0;
ch[u][c]=sz++;
}
u=ch[u][c];
}
val[u]=v;//单词末尾节点设特殊值,也可统计次数等
}
int f[maxn],last[maxn];//后缀链接
queue<int> q;
void getfail()
{
f[0]=0;
for(int c=0;c<sigma_size;c++)
{
int u=ch[0][c];
if(u)
{
f[u]=0;
q.push(u);
last[u]=0;
}
}
while(!q.empty())
{
int r=q.front();q.pop();
for(int c=0;c<sigma_size;c++)
{
int u=ch[r][c];
if(!u)
{
ch[r][c]=ch[f[r]][c];
continue;
}
q.push(u);
int v=f[r];
while(v&&!ch[v][c]) v=f[v];
f[u]=ch[v][c];
last[u]=val[f[u]]?f[u]:last[f[u]];
}
}
}
ll d[maxn*10][maxn][maxn];
int c=1,len[maxn];//长度,ac自动机位置,日记本页数
int go(int t,int& pos,int& now)
{
int l=len[t];
//cout<<pos<<' '<<now<<' '<<endl;
for(int i=0;i<l;i++)
{
int id=idx(s[t][i]);
int x=mp[now][id];
// cout<<id<<' '<<x<<' '<<now<<' '<<pos<<endl;
now=nxt[now][id];
pos=ch[pos][x];
if(val[pos]||last[pos]) return false;
}
// cout<<pos<<' '<<now<<endl<<endl;
return true;
}
ll num[maxn*10];
int main()
{
//freopen("tte.txt","r",stdin);
scanf("%d%d",&n,&m);
char cc[5];
init();
for(int i=0;i<26;i++)
{
for(int j=1;j<=n;j++)
{
int u;
scanf("%s",cc);
if(strlen(cc)==3)
u=(cc[1]-'0')*10+(cc[2]-'0');
else u=(cc[1]-'0');
int v=cc[0]-'a';
mp[j][v]=i;
nxt[j][v]=u;
}
}
while(scanf("%s",s[c])==1)
{
len[c]=strlen(s[c]);
insert(s[c],1);
c++;
}
c-=1;
getfail();
d[0][0][1]=1;
for(int i=0;i<m;i++)
{
for(int j=0;j<sz;j++)
{
for(int k=1;k<=n;k++)
{
if(d[i][j][k])
{
for(int l=1;l<=c;l++)
{
if(i+len[l]<=m)
{
int now=k,pos=j;
bool ok=go(l,pos,now);
if(!ok) continue;
d[i+len[l]][pos][now]+=d[i][j][k];
d[i+len[l]][pos][now]%=mod;
num[i+len[l]]+=d[i][j][k];
num[i+len[l]]%=mod;
}
}
}
}
}
}
for(int i=1;i<=m;i++)
{
printf("%lld\n",num[i]);
}
return 0;
}
poj2778
AC自动机+矩阵快速幂
居然是离散数学结论题:在离散数学,我们可以知道:要求出一个图中第i个点与第j个点的可达关系,我们可以通过将这张图的邻接矩阵A求T次幂,求幂的结果即为第i个点走T步达到结点j的路径个数。
于是用AC自动机预处理,于是题目转化位从根节点出发走n步且不经过单词节点的方案数。把节点间关系转化为邻接矩阵表示,利用矩阵快速幂n次方。 ∑ i = 0 n a n s [ 0 ] [ i ] \sum\limits_{i=0}^{n}ans[0][i] i=0∑nans[0][i]即为答案。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<set>
#include<queue>
#include<cstring>
#include<string>
#include<cmath>
#include<vector>
#include<stack>
#include<map>
using namespace std;
using namespace std;
const int maxn=100+5;
const int mod=1e5;
const int sigma_size=4;
int ch[maxn][sigma_size],val[maxn],sz;
int mp[maxn][maxn];
int idx(char c)
{
if(c=='A') return 0;
if(c=='T') return 1;
if(c=='C') return 2;
if(c=='G') return 3;
}
void init()//啊啊啊,一定要调用一下
{
sz=1;
memset(ch[0],0,sizeof(ch[0]));
memset(val,0,sizeof(val));
}
void insert(char *s,int v)
{
int u=0,len=strlen(s);
for(int i=0;i<len;i++)
{
int c=idx(s[i]);
if(!ch[u][c])
{
memset(ch[sz],0,sizeof(ch[sz]));
val[sz]=0;
ch[u][c]=sz++;
}
u=ch[u][c];
}
val[u]=v;//单词末尾节点设特殊值,也可统计次数等
}
int f[maxn],last[maxn];//后缀链接,上一个单词节点
queue<int> q;
int getfail()
{
f[0]=0;
for(int c=0;c<sigma_size;c++)
{
int u=ch[0][c];
if(u)
{
f[u]=0;
q.push(u);
last[u]=0;
}
}
while(!q.empty())
{
int r=q.front();q.pop();
for(int c=0;c<sigma_size;c++)
{
int u=ch[r][c];
if(!u)
{
ch[r][c]=ch[f[r]][c];
continue;
}
q.push(u);
int v=f[r];
while(v&&!ch[v][c]) v=f[v];
f[u]=ch[v][c];
last[u]=val[f[u]]?f[u]:last[f[u]];
val[u]|=val[f[u]];
}
}
}
char s[maxn];
int a[maxn][maxn],ans[maxn][maxn],tmp[maxn][maxn];
void martrix_mul(int a[][maxn],int b[][maxn],int n,int mod)
{
for(int i=0;i<=n;i++)
{
for(int j=0;j<=n;j++)
{
tmp[i][j]=0;
for(int k=0;k<=n;k++)
{
tmp[i][j]+=1ll*a[i][k]*b[k][j]%mod;
tmp[i][j]%=mod;
}
}
}
memcpy(a,tmp,sizeof(tmp));
}
void martrix_pow(int a[][maxn],int ans[][maxn],int y,int n,int mod)
{
for(int i=0;i<=n;i++) ans[i][i]=1;
while(y)
{
if(y&1)
{
martrix_mul(ans,a,n,mod);
}
y>>=1;
martrix_mul(a,a,n,mod);
}
}
int main()
{
//freopen("tte.txt","r",stdin);
int m,n;
init();
scanf("%d%d",&m,&n);
for(int i=1;i<=m;i++)
{
scanf("%s",s);
insert(s,1);
}
getfail();
for(int i=0;i<=sz;i++)
{
for(int j=0;j<4;j++)
{
if(!val[ch[i][j]])
mp[i][ch[i][j]]+=1;
}
if(val[i])
{
memset(mp[i],0,sizeof(mp[i]));
}
}
/*for(int i=0;i<=sz;i++)
{
for(int j=0;j<=sz;j++)
{
printf("%d ",mp[i][j]);
}
printf("\n");
}*/
martrix_pow(mp,ans,n,sz,mod);
int res=0;
for(int i=0;i<=sz;i++)//肯定从根出发啊
{
res+=ans[0][i];
res%=mod;
}
printf("%d\n",res);
return 0;
}
UVA11019
题目大意:给出一个n*m的字符矩阵T,你的任务是找出给定的x *y的字符矩阵P出现了多少次?
蓝书上一道题,利用AC自动机解决多模板匹配,同时有一些技巧,把P每一行拆出建立AC自动机,在用T的每一行来匹配,当P的第i行出现在T的第r行,起始列编号为c时,意味着 c o u n t [ r − i ] [ j ] count[r-i][j] count[r−i][j]应该加1, c o u n t [ i ] [ j ] count[i][j] count[i][j]代表以(i,j)为左上顶点与P相同大小的矩阵与P匹配了多少行。
//please ac
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<set>
#include<queue>
#include<cstring>
#include<string>
#include<cmath>
#include<vector>
#include<stack>
#include<map>
using namespace std;
typedef long long ll;
const int maxn=1000*1000+5;
const int sigma_size=26;
int ch[maxn][sigma_size],val[maxn],sz;
int f[maxn],last[maxn],nxt[1005];//后缀链接
int idx(char c)
{
return c-'a';
}
void init()
{
sz=1;
memset(ch[0],0,sizeof(ch[0]));
memset(val,0,sizeof(val));
}
void insert(char *s,int v)
{
int u=0,len=strlen(s);
for(int i=0;i<len;i++)
{
int c=idx(s[i]);
if(!ch[u][c])
{
memset(ch[sz],0,sizeof(ch[sz]));
val[sz]=0;
ch[u][c]=sz++;
}
u=ch[u][c];
}
nxt[v]=val[u];
val[u]=v;//单词末尾节点设特殊值,也可统计次数等
}
int n,m,x,y;
char T[1005][1005],s[105];
int cnt[1005][1005];
queue<int> q;
void getfail()
{
f[0]=0;
for(int c=0;c<sigma_size;c++)
{
int u=ch[0][c];
if(u)
{
f[u]=0;
q.push(u);
last[u]=0;
}
}
while(!q.empty())
{
int r=q.front();q.pop();
for(int c=0;c<sigma_size;c++)
{
int u=ch[r][c];
if(!u)
{
ch[r][c]=ch[f[r]][c];
continue;
}
q.push(u);
int v=f[r];
while(v&&!ch[v][c]) v=f[v];
f[u]=ch[v][c];
last[u]=val[f[u]]?f[u]:last[f[u]];
}
}
}
void solve(int c,int r,int h)
{
for(int u=val[c];u!=0;u=nxt[u])
{
if(r-u+1>=1) cnt[r-u+1][h]++;
}
}
void find(char *T,int r)//在T中找模板
{
int len=strlen(T);
int j=0;
for(int i=0;i<len;i++)
{
int c=idx(T[i]);
j=ch[j][c];
if(val[j])
{
solve(j,r,i+2-y);
}
else if(last[j])
{
solve(last[j],r,i+2-y);
}
}
}
int main()
{
//freopen("tte.txt","r",stdin);
//freopen("out.txt","w",stdout);
int t;scanf("%d",&t);
while(t--)
{
init();
memset(cnt,0,sizeof(cnt));
memset(nxt,0,sizeof(nxt));
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%s",T[i]+1);
scanf("%d%d",&x,&y);
for(int i=1;i<=x;i++)
{
scanf("%s",s+1);
insert(s+1,i);
}
getfail();
for(int i=1;i<=n;i++)
{
find(T[i]+1,i);
}
int ans=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(cnt[i][j]==x) ans++;
}
}
printf("%d\n",ans);
}
return 0;
}
fail树的应用:洛谷P2414
把每次打印的串建立AC自动机,顺便建立fail树。查询第x个字符串在第y个字符串中出现了几次,其实就是找到fail树中x的单词节点的子树中有多少个点属于y.维护子树中内容,想到可以使用dfs序用树状数组或线段树维护。那么对于查询我们也可以用trie树来维护y,查询时就dfs trie树,进入单点更新给对应节点+1,退出单点更新-1,遇到单词节点查询对应x节点子树权值和,赋给对应查询答案。
做本题时,到想到线段树维护dfs序都比较顺利,用trie维护查询想了挺久,代码敲了半天,但感觉实现起来还是有很多注意的,改的有点自闭,之后重新想一想再把代码补上吧。