一、KMP
1、处理范围:
给你一个长度为N的字符串(主串),再给你一个长度为M(M
2、主体思想:
next[i]表示当我们匹配到模式串位置i失败了的时候,我们应该换到模式串的哪个位置继续进行匹配
因此失配时,通过不断的跳next来降低时间复杂度
3、代码实现:
int s1[maxn],s2[maxn],nxt[maxn];
void getnext(){
int l=-1,r=0;
nxt[0]=-1;
while(r<lens2){
if(l==-1||s2[l]==s2[r]){
l++,r++;
nxt[r]=l;
}
else l=nxt[l];
}
}
bool kmp_yon(){
int t1=0,t2=0;
while(t1<lens1&&t2<lens2){
if(t2==-1||s1[t1]==s2[t2])
t1++,t2++;
else t2=nxt[t2];
}
if(t2==lens2)return 1;
return 0;
}
int kmp_num(){
int t1=0,t2=0,times=0;
while(t1<lens1){
if(t2==-1||s1[t1]==s2[t2])
t1++,t2++;
else t2=nxt[t2];
if(t2==lens2){
times++;
t2=nxt[t2];
}
}
return times;
}
4、知识链接:
KMP与循环节
如果一个长度为L的字符串,他的长度为x的前缀和长度为x的后缀长度相同,那么它长度为L-x的前缀是这个串的一个循环节
其中循环节的定义为:若A是B的循环节,则B可以表示成若干个A顺次拼接而成的前缀
例:求一个串所有前缀的循环节
int n,nxt[1000005];
char a[1000005];
void getnext(){
int l=-1,r=0;
nxt[0]=-1;
while(r<=n){
if(l==-1||a[l]==a[r]){
l++,r++;
nxt[r]=l;
}
else l=nxt[l];
}
}
int main(){
fin("c");fout("c");
n=read();
scanf("%s", a);
getnext();
rep(i,2,n){
if((i%(i-nxt[i]))==0&&i-nxt[i]<i){
printf("%d %d\n", i, i/(i-nxt[i]));
}
}
return 0;
}
5、例题选讲:
[Noi2014]动物园
可以先正常求next数组,并维护以下每个点跳几次next数组会到头,然后维护一个num指针,用几乎一样的方式来进行匹配,区别在于一旦越过中线就额外跳一次next
#include<bits/stdc++.h>
#define ll long long
#define idg isdigit
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define dep(i,a,b) for(int i=a;i>=b;i--)
#define fed(i,x) for(int i=head[x];i!=-1;i=e[i].nxt)
#define rd1 inline int read(){int x=0,f=1;char ch=getchar();
#define rd2 for(;!idg(ch);ch=getchar())if(ch=='-')f=-1;
#define rd3 for(;idg(ch);ch=getchar())x=x*10+ch-'0';return x*f;}
#define fastread rd1 rd2 rd3
#define rd(x) x=read()
#define fin(x) freopen(x".in","r",stdin)
#define fout(x) freopen(x".out","w",stdout)
#define inf 0x3f3f3f3f
#define maxn 1000005
#define wxh 1000000007
using namespace std;
fastread
int nxt[maxn],num[maxn],n;
char ch[maxn];
int main(){
int rd(T);
while(T--){
scanf("%s", ch);
ll ans=1;
num[0]=0,num[1]=1;
nxt[0]=-1,nxt[1]=0;
n=strlen(ch);
int tmp=0;
rep(i,1,n-1){
while(tmp>=0&&ch[i]!=ch[tmp])tmp=nxt[tmp];
nxt[i+1]=++tmp;
num[i+1]=num[tmp]+1;
}
tmp=0;
rep(i,1,n-1){
while(tmp>=0&&ch[i]!=ch[tmp])tmp=nxt[tmp];
++tmp;
while((tmp<<1)>(i+1))tmp=nxt[tmp];
(ans*=(ll)num[tmp]+1)%=wxh;
}
printf("%lld\n", ans);
}
}
二、trie树
1、构建意义
维护一个数组ch[N][character_size],代表这个节点所有儿子的编号
其中一般character_size为26(有时为2,有时为M)
每当要插入一个新字符串时,就从根沿着路径向下走,如果没有这个节点就新建一个接在下面
Trie树可以用来检验一个字符串是否出现过(或是否为现有某个字符串的前缀),可以把多个串合理地连接
太简单了,不再多讲
三、AC自动机
就是Trie和KMP的结合体
与Trie的区别:多了nxt数组,并且将整棵树重构
与KMP的区别:可以有多个模式串一起匹配
实际形态:先将所有的模式串建成一颗Trie树,然后在这棵树上构建nxt数组
1、几个定义:
nxt[i] 类似于KMP当中的nxt(fail)数组,代表的是从根到这个节点组成的路径中最长的存在的后缀的结束节点
ch[i][j] 与正常字典树不同,这里为重构树之后的儿子节点,意义是从i号节点再匹配一个字符j之后所应该到达的节点
last[i] 从根到这个节点路径所形成的字符串的最长的是模式串的后缀(不包括自己)的结束节点
2、具体做法:
先建出Trie树(*)
然后BFS这棵树
对于队头的节点x
我们依次查看他的26个儿子
如果没有这个儿子
——就为ch[x][i]赋上对应的值
否则
——根据nxt[x]和last[x]
——计算出ch[x][i]的nxt和last值
对于匹配串,直接在AC自动机上跑一遍就可以了
因为每个节点的26个儿子已经被重新赋过值了
所以只需要O(匹配串长度)模拟一遍走过的路径就可以了
(这也是为什么他叫做自动机的原因)
(这也是为什么许多AC自动机的题最后都变成图论题的原因)
每当走到一个节点时,我们可以用暴力的方式更新答案
也可以打上一个标记,到最后统一计算答案
3、代码实现:
求有多少个匹配串在主串里
#include<bits/stdc++.h>
#include<queue>
#define ll long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define dep(i,a,b) for(int i=a;i>=b;i--)
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int n,cnt;string s;
struct trie{
int vis[26],fail,num;
}t[1000005];
void build_trie(string s){
int le=s.length(),p=0;
rep(i,0,le-1){
if(!t[p].vis[s[i]-'a'])
t[p].vis[s[i]-'a']=++cnt;
p=t[p].vis[s[i]-'a'];
}
t[p].num++;//!!!因为匹配串可能有重复,所以要记录有几个这样的串
}
void init_fail(){
queue<int> q;
rep(i,0,25){//第二层的fail指针提前处理为0
if(t[0].vis[i]){
t[t[0].vis[i]].fail=0;
q.push(t[0].vis[i]);
}
}
while(!q.empty()){
int fr=q.front();q.pop();
rep(i,0,25){
if(t[fr].vis[i]){
t[t[fr].vis[i]].fail=t[t[fr].fail].vis[i];
q.push(t[fr].vis[i]);
}
else t[fr].vis[i]=t[t[fr].fail].vis[i];
}
}
}
int ac_(string s){
int l=s.length(),p=0,ans=0;
rep(i,0,l-1){
p=t[p].vis[s[i]-'a'];
for(int j=p;j&&t[j].num!=-1;j=t[j].fail){
ans+=t[j].num;t[j].num=-1;
}
}
return ans;
}
int main(){
n=read();
rep(i,1,n){
cin>>s;
build_trie(s);
}
t[0].fail=0;
init_fail();
cin>>s;
printf("%d\n", ac_(s));
return 0;
}
求出现次数最多的匹配串(也可以求每个匹配串的出现次数,即final[i].tot)
#include<bits/stdc++.h>
#include<queue>
#define ll long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define dep(i,a,b) for(int i=a;i>=b;i--)
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int n,cnt;string s[200];
struct trie{
int vis[26],fail,num;
}t[1000005];
void reset(int now){
memset(t[now].vis,0,sizeof(t[now].vis));
t[now].fail=0;
t[now].num=0;
}
void build_trie(string s,int bianhao){
int le=s.length(),p=0;
rep(i,0,le-1){
if(!t[p].vis[s[i]-'a']){
t[p].vis[s[i]-'a']=++cnt;
reset(cnt);
}
p=t[p].vis[s[i]-'a'];
}
t[p].num=bianhao;
}
void init_fail(){
queue<int> q;
rep(i,0,25){//第二层的fail指针提前处理为0
if(t[0].vis[i]){
t[t[0].vis[i]].fail=0;
q.push(t[0].vis[i]);
}
}
while(!q.empty()){
int fr=q.front();q.pop();
rep(i,0,25){
if(t[fr].vis[i]){
t[t[fr].vis[i]].fail=t[t[fr].fail].vis[i];
q.push(t[fr].vis[i]);
}
else t[fr].vis[i]=t[t[fr].fail].vis[i];
}
}
}
struct ans{
int id,tot;
}final[200];
void ac_(string s){
int l=s.length(),p=0;
rep(i,0,l-1){
p=t[p].vis[s[i]-'a'];
for(int j=p;j&&t[j].num!=-1;j=t[j].fail){
final[t[j].num].tot++;
}
}
}
bool cmp(ans A,ans B){
if(A.tot!=B.tot)return A.tot>B.tot;
return A.id<B.id;
}
int main(){
while(~scanf("%d", &n)&&n){
cnt=0;reset(0);
rep(i,1,n){
cin>>s[i];
final[i].id=i;
final[i].tot=0;
build_trie(s[i],i);
}
t[0].fail=0;
init_fail();
cin>>s[0];
ac_(s[0]);
sort(final+1,final+1+n,cmp);
int mx=final[1].tot;
printf("%d\n", mx);
rep(i,1,n){
if(final[i].tot==mx){
cout<<s[final[i].id]<<endl;
}
else break;
}
}
return 0;
}
4、last树:
我们可以发现,所有节点的last放在一起可以构成一棵树
这就使得我们可以像线段树一样在上面进行打标记的操作
例如下面的hdu 2222,每次暴力跳last时间复杂度不能保证
我们就可以通过先在某个点打上标记,当所有操作完成后,按照顺序自叶子至根把标记pushdown到父节点上
这样时间复杂度就是线性的了
5、例题选讲:
HDU2222:Keywords Search
先建出AC自动机,然后让匹配串在上面跑一遍
每当走到一个节点x,
暴力做法:一直通过跳到last[x]来把所有以这个点为结尾的串都打上“访问过”标记
保证复杂度的做法:为这个点打上一个“访问过”标记(最后倒着pushdown回来)
最后我们只需要数一下有多少串被访问过即可
[Poi2000]病毒
先建出来AC自动机
我们称所有模式串的结束节点和last[x]有值的节点为危险节点
无限长安全代码存在当且仅当存在一条无限长路径永远都不经过危险节点
无限长的路径–>必然有环的存在
即在AC自动机上寻找一个包含根且不包含危险节点的环
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
using namespace std;
int n,len,siz, s[30005][2], fai[30005], u, v;
string a;
queue<int> d;
bool vis[30005], tmp[30005],val[30005];
void add() {
u=0;
len=a.length();
rep(i,0,len-1){
v=a[i]-'0';
if(!s[u][v])s[u][v]=++siz;
u=s[u][v];
}
val[u]=1;
}
void getFail() {
if(s[0][0])d.push(s[0][0]);
if(s[0][1])d.push(s[0][1]);
while(!d.empty()) {
u=d.front();
d.pop();
rep(i,0,1) {
v=s[u][i];
if(v) {
fai[v]=s[fai[u]][i];
d.push(v);
val[v]|=val[fai[v]];
} else s[u][i]=s[fai[u]][i];
}
}
}
bool dfs(int x) {
tmp[x]=1;
rep(i,0,1) {
u = s[x][i];
if(tmp[u])return 1;
if(vis[u]||val[u])continue;
vis[u]=1;
if(dfs(u))return 1;
}
tmp[x]=0;
return 0;
}
int main() {
ios::sync_with_stdio(false);
cin>>n;
rep(i,1,n){cin>>a;add();}
getFail();
if(dfs(0))cout<<"TAK\n";
else cout<<"NIE\n";
return 0;
}
[JSOI2007]文本生成器
满足条件的字符串总数=26^M-不包含这N个字符串的串的数量
设F[i][j]表示匹配前i个字母之后走到了AC自动机的j号节点的方案数
则有对于任意的j的儿子节点x
若x不是危险节点,则F[i+1][x]+=F[i][j]
最后把所有不是危险节点的F[M][x]求和即是不包含这N个字符串的串的数量
用26^M减去即可
黄学长的代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#define mod 10007
using namespace std;
int n,m,sz=1,ans1,ans2=1;
int a[6001][27],point[6001],q[6001],f[101][6001];
char s[101];
bool danger[6001];
void ins()
{
int now=1,c;
for(int i=0;i<strlen(s);i++)
{
c=s[i]-'A'+1;
if(a[now][c])now=a[now][c];
else now=a[now][c]=++sz;
}
danger[now]=1;
}
void acmach()
{
int t=0,w=1,now;
q[0]=1;point[1]=0;
while(t<w)
{
now=q[t++];
for(int i=1;i<=26;i++)
{
if(!a[now][i])continue;
int k=point[now];
while(!a[k][i])k=point[k];
point[a[now][i]]=a[k][i];
if(danger[a[k][i]])
danger[a[now][i]]=1;
q[w++]=a[now][i];
}
}
}
void dp(int x)
{
for(int i=1;i<=sz;i++)
{
if(danger[i]||!f[x-1][i])continue;
for(int j=1;j<=26;j++)
{
int k=i;
while(!a[k][j])k=point[k];
f[x][a[k][j]]=(f[x][a[k][j]]+f[x-1][i])%mod;
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=26;i++)a[0][i]=1;
for(int i=1;i<=n;i++)
{
scanf("%s",s);
ins();
}
acmach();
f[0][1]=1;
for(int i=1;i<=m;i++)dp(i);
for(int i=1;i<=m;i++)
ans2=(ans2*26)%mod;
for(int i=1;i<=sz;i++)
if(!danger[i])ans1=(ans1+f[m][i])%mod;
printf("%d",(ans2-ans1+mod)%mod);
return 0;
}
[HNOI2008]GT考试
虽然只有一个串,不过我们还是可以强行建出AC自动机
然后问题便成了一道图论题,有M个节点,每个节点有10个出边,问有多少种不同的走法使得走N步之后仍然不走到M号点
Dp[i][j]表示走了i步,当前在j号节点的方案数
我们发现从Dp[i]转移到Dp[i+1]时,转移的方程都是一样的
所以矩阵乘法快速幂加速一发就可以了
[HNOI2004]L语言
设F[i]表示长度为i的前缀是否可表示
由于单词长度<=10,所以我们可以暴力转移
即F[i]=(F[i-1]&can(i,i))|(F[i-2]&can(i-1,i))|…|(F[i-10]&can(i-9,i))
其中can(i,j)表示i到j这段子串是否是一个单词
这一步我们可以在AC自动机上O(j-i+1)跑一遍来得到
[Usaco2015 Feb]Censoring
首先先建出AC自动机
接着我们发现在匹配到单词节点的时候,我们会出现一次倒档的操作,也就是说我们需要回到这个单词开始的位置继续对后面的字符进行匹配
所以我们考虑维护一个栈,里面存放着FJ还没删的这些字符分别对应AC自动机的哪些节点
然后每当到一个单词节点,我们直接把这一段弹栈,然后从栈顶继续匹配
四、对偶图
略