KMP,trie,AC自动机,topo优化
\(KMP\)
前缀函数
给定一个长度为 \(n\) 的字符串 \(s\) ,其前缀函数定义为一个长度为 \(n\) 的数组 \(\pi\) 。
其中 \(\pi[i]\) 的定义为:
- 如果子串 \(s[0...i]\) 有一对相等的真前缀 \(s[0...k-1]\) 与真后缀 \(s[i-k+1...i]\) ,则 \(\pi[i]\)就是这个真前(后)缀的长度,即 \(\pi[i]=k\) 。
- 若子串 \(s[0...i]\) 有不止一对相等的真前缀与真后缀,则 \(\pi[i]\) 为其中最长的那一对的长度。
- 若没有相等的真前缀与真后缀,则 \(\pi[i]=0\)。
综上: \(\pi[i]=\max\limits_{k=0}^{i}\{k|s[0...k-1]=s[i-k+1...i]\}\)
前缀函数的求法--\(KMP\)
很容易想到 \(O(n^3)\) 的做法,即枚举每个 \(k\) ,显然这不是理想的做法。
Knuth,Morris,Pratt在1977年共同发布了一个算法,简记为 \(KMP\) 算法。
求前缀函数为该算法的一个典型应用。
基本思想:
假设当前求解到 \(s[i]\) 。
显然,\(s[i-1]=s[\pi[i-1]]\),若 \(s[i]=s[\pi[i-1]+1]\) 则 \(\pi[i]=\pi[i-1]+1\) 。
\(\overbrace{s_1 s_2 s_3 ... } ^{0...\pi[i-1]} s_4 s_5... \overbrace{s_1 s_2 s_3 ...} ^{^{i-\pi[i]-1...i-1}}s_4\)
那么若 \(s[i]\ne s[\pi[i-1]+1]\) 又如何呢?
同上 \(s[i-1]=s[\pi[i-1]]=s[\pi[\pi[i-1]]]=...=s[\pi^j[i-1]]\),我们只需继续向前跳 \(\pi\) 数组即可。
\(\overbrace{ \underbrace{s_1 s_2 s_3 ...}_{0...\pi[\pi[i-1]]} s_4 s_1 s_2 s_3 ...}^{0...\pi[i-1]} s_5 \overbrace{s_1 s_2 s_3...}^{^{i-\pi[i-1]-1...i-1}} s_4\)
实现代码:
for(int i=1,j=0;i<s.length();i++){
j=nxt[i-1];
while(j&&s[j]!=s[i])j=nxt[j-1];
j+=(s[j]==s[i]);nxt[i]=j;
}
for(int i=0;i<s.length();i++)cout<<nxt[i]<<' ';cout<<endl;
KMP代码的应用
给定两个个字符串 \(s,t\) ,长度分别为 \(n,m\) 。
求 \(s\) 在 \(t\) 中出现多少次。
对 \(s+'\#'+t\) 跑一遍 \(KMP\) ,其中 \(\#\) 为 \(s,t\) 中均未出现的字符, \(\pi[i]=n\) 的位置即为 \(s[n-1]\) 在 \(t\) 中出现的位置。统计即可。
cin>>s>>t;
n=s.length();
s=s+"#"+t;
for(int i=1,j=0;i<s.length();++i){
j=nxt[i-1];
while(j&&s[j]!=s[i])j=nxt[j-1];
j+=(s[j]==s[i]);nxt[i]=j;
}
for(int i=0;i<s.length();++i){
if(nxt[i]==n)++ans;
}
\(Trie\)
为了存储大量字符串,我们可以建一棵树。
其中红色点为字符串结束标志,标记此节点与其祖先构成了一个字符串。
下图的 \(Trie\) 储存了 \(xie,xor,he,her,his\) 五个字符串。
很容易发现两字符串最长公共字串长度即为两字符串结尾节点的 \(lca\) 的深度。
void insert(char *s,int len){
int u=0,c;
for(int i=1;i<=len;++i){
c=s[i]-'a';
if(!ch[u][c])ch[u][c]=++cnt;
u=ch[u][c];
}
++b[u];//字符串的末尾,不同的题使用不同的记录法
}
AC自动机
AC自动机是一种将 \(KMP\) 与 \(trie\) 的算法。
insert函数
即为 \(trie\) 的插入。
build函数
即为求AC自动机的后缀数组fail。
queue<int>q;
void build(){
for(int i=0;i<26;i++)
if(ch[0][i])q.push(ch[0][i]);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++)
if(ch[u][i]){
fail[ch[u][i]]=ch[fail[u]][i];
q.push(ch[u][i]);
}
else ch[u][i]=ch[fail[u]][i];//----------------优化,省略了从u跳到fail[u]的时间
}
return;
}
query函数
int query(char *t,int l) {
int u=0,c;
for(int i=1;i<=l;i++){
c=t[i]-'a';
u=ch[u][c];
for(int j=u;j;j=fail[j])
ans+=b[j];
}
return ans;
}
\(topo\) 优化
基于P3808
在AC自动机查询答案跳fail的时候,有的时候会跳很多遍。
把fail数组当作边,使用 \(topo\) 排序,直接在 \(topo\) 的时候将答案传递。
build函数
queue<int>q;
void build(){
for(int i=0;i<26;i++)
if(ch[0][i])q.push(ch[0][i]);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++)
if(ch[u][i]){
fail[ch[u][i]]=ch[fail[u]][i];
++indeg[fail[ch[u][i]];
q.push(ch[u][i]);
}
else ch[u][i]=ch[fail[u]][i];
}
}
query函数
void query(){
int u=0,c;
for(int i=1;i<=l;i++){
c=t[i]-'a';
u=ch[u][c];
ans[u]+=b[j];//----------------不用跳了
}
}
topo函数统计答案
int Ans;
void topo(){
for(int i=1;i<=cnt;i++)if(indeg[i]==0)q.push(i);
while(!q.empty()){
int u=q.front();q.pop();
if(u)ans[fail[u]]+=ans[u];
Ans=max(Ans,ans[fail[u]]);
if(!(--indeg[fail[u]]))q.push(fail[u]);
}
}