啥??kmp还有自动机??
没错,我们经常听到ac自动机,后缀自动机之类的,kmp自动机我还真是第一次见。。。
前置知识:https://oi-wiki.org/string/kmp/ (kmp都不会还学啥kmp自动机)
我接触到这个知识点是由于我碰见了这么一个题目:Codeforces 808G
https://codeforces.com/contest/808/problem/G
中文题意在这:https://www.luogu.com.cn/problem/CF808G
最开始的想法是:设表示当前在S串的第i个位置,已经匹配了T串的前j个字符时(也就是对于任意的 ,有)T串出现次数的最大值
那么转移的时候,考虑在S串的第i+1个位置插入字符ch,然后就发现:这不就跟kmp里的两个字符串匹配时,模式串T利用next函数进行转移的情况一样嘛
然后就有以下的转移方程:
memset(f,-0x3f,sizeof(f));
f[0][0]=0;
for(int i=0;i<=n-1;i++){
for(int j=0;j<=m;j++){
char l='a',r='z';
if(s[i+1]!='?') l=r=s[i+1];
for(char ch=l;ch<=r;ch++){//枚举s[i+1]放什么字符
int nj=j;
while(nj>0&&(nj==m||t[nj+1]!=ch)) nj=nxt[nj];
if(t[nj+1]==ch) nj++;//kmp的匹配过程
f[(i+1)&1][nj]=max(f[(i+1)&1][nj],f[i&1][j]+(nj==m));
//nj==m说明t出现了一次,这里用了滚动数组
}
}
for(int j=0;j<=m;j++) f[i][j]=-inf;
}
愉快地交上去,发现:
T飞了啊!!
问题出在哪?
答案就是每次枚举j的时候,都是暴力的从j跳到nxt[j],如果遇到这样的情况:
bbbbbbbbb..(此处有100个b)
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...(此处有100000个a)
复杂度就被卡成o(n^m^2)的了。。
为了解决这个问题,就引入了一个新数组,表示模式串T的前i个位置已经全匹配上了,在第i+1个位置要匹配字符c的时候,经过kmp的next函数转移到的位置
如果已经得到nxtc数组的话,那么就直接可以o(1)转移j了
就像这样:
memset(f,-0x3f,sizeof(f));
f[0][0]=0;
for(int i=0;i<=n-1;i++){
for(int j=0;j<=m;j++){
char l='a',r='z';
if(s[i+1]!='?') l=r=s[i+1];
for(char ch=l;ch<=r;ch++){
int nj=nxt[j][ch];//就是这里进行o(1)转移
f[(i+1)&1][nj]=max(f[(i+1)&1][nj],f[i&1][j]+(nj==m));
}
}
for(int j=0;j<=m;j++) f[i][j]=-inf;
}
那怎么得到nxtc数组呢??
暴力的写法是这样:
for(int o=0;o<=25;o++){
for(int i=0;i<=m;i++){
int ni=i;
while(ni>0&&(ni==m||t[ni+1]!='a'+o)) ni=nxt[ni];
if(t[ni+1]=='a'+o) ni++;
nxtc[i][o]=ni;
}
}
最坏情况依旧是o(m^2),一样会被T飞。。
而且这样写的话发现 i-1的nxtc数组完全没用上啊,我们可不可以用动态规划的思想利用已经求得的前i-1的nxtc去求当前i的nxtc
其实到这里已经很明显了:
假如已经知道了,那么在i+1的位置匹配c时有两种情况:
1.一种是,那么
2.另一种是,那么
(这里AC自动机的fail指针的求解非常得像,可以说kmp自动机是AC自动机的基础)
i=0时初始值为:
如果,则
否则
for(int i=0;i<=m;i++){
for(int o=0;o<=25;o++){
if(t[i+1]=='a'+o) nxtc[i][o]=i+1;
else{
if(i==0) nxtc[i][o]=0;
else nxtc[i][o]=nxtc[nxt[i]][o];
}
}
}
然后就愉快地AC了!
(oiwiki里原来有kmp自动机及其转移函数定义,是我孤陋寡闻了。。