KMP/exKMP
前言:
好多话想说,但没劲说了。本人写这篇笔记的时候脑子已罢工。这周是真难熬,,
字符串这玩意比想象的难搞的多……
KMP算法:
模板:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x7fffffff
const ll maxn=2000010;
ll kmp[maxn],g;
char a[maxn],b[maxn];
inline ll in() {
char a=getchar();
ll t=0,f=1;
while(a<'0'||a>'9') {if (a=='-') f=-1;a=getchar();}
while(a>='0'&&a<='9') {t=(t<<1)+(t<<3)+a-'0';a=getchar();}
return t*f;
}
signed main() {
scanf("%s%s",a+1,b+1);
ll lena=strlen(a+1),lenb=strlen(b+1);
for (ll i=2;i<=lenb;++i) {
while (g&&b[g+1]!=b[i]) g=kmp[g];
if (b[g+1]==b[i]) g++;
kmp[i]=g;
}
g=0;
for (ll i=1;i<=lena;++i) {
while (g&&b[g+1]!=a[i]) g=kmp[g];
if (a[i]==b[g+1]) g++;
if (g==lenb) printf("%lld\n",i-lenb+1);
}
for (ll i=1;i<=lenb;++i) printf("%lld ",kmp[i]);
return 0;
}
核心思想:
首先预处理模式串的最长前后缀,之后只移动模式串的指针根据我们预处理出的前后缀(加速)进行匹配。
预处理前后缀的作用是加速,也是整个算法的核心。
系列技巧(做题笔记):
-
字符串最小循环节(周期) \(=\) 字符串长度 \(-\) 最长公共前后缀
模板题:[BOI2009]Radio Transmission 无线传输
-
在 kmp 过程中和栈结合
要记录消除所有模式串之后的文本串,我们可以用一个栈记录当前我们遍历到的所有字符,若匹配成功,将栈顶弹出 \(len\) 长度即可(\(len\) 是当前匹配成功的模式传长度)。但是可能我们把这个当前匹配好的模式串从文本中“模拟删除后”,又产生了新的模式串子串,所以我们还要记录每一个 \(i\) 跳到的位置时的 \(g\) 值(\(i,g\) 含义就是 kmp 模板匹配部分的含义)。当当前模式串被删去后,再将 \(g\) 跳回栈顶对应的 \(g\) 即可。
-
求字符串中所有的循环节
求字符串有多少种循环节/最小/最长循环节方法:
最小循环节 \(=len-next[len]\),则次小循环节 \(= len-next[next[len]]\) ,以此类推直到能求到最大循环节。
例题:[POI2006] OKR-Periods of Words:求最大循环节
根据刚才提到的定理,我们把所有合法前缀 \(j\)(\(j\) 代表这个字符串最右端的下标) 一直跳 \(next[j]\) 直到 \(next[j]=0\) 为止。
-
失配树——求周期(跳 \(next\))时树上倍增优化
我们一层一层跳显然太慢,最坏复杂度为 \(O(n^2)\)。
比如这个字符串:\(aaaaaaaaaaaaaaaaaaaaaaaa\)。
怎么办呢?
观察到对于每一个前缀 \(j\),他都可以逐级跳 \(next[j]\) 跳到 \(next[j]\) 为 \(0\) 的位置。于是我们可以将 \(next\) 构建为一个树形结构(即失配树),对于每一个 \(j\) 其父亲为 \(next[j]\)。
所以我们逐级跳 \(next[j]\) 的过程就是在失配树上不断寻找他的祖先,所以这一过程我们就可以采用倍增的方式优化:
仍然是预处理最长前后缀,只不过这回要开二维(记录他的第 \(2^n\) 级祖先)了:
g=0;
for (ll i=2;i<=n;++i) {
while (g&&s[i]!=s[g+1]) g=F[0][g];
if (s[g+1]==s[i]) g++;
F[0][i]=g,dis[i]=dis[g]+1;
}
再用倍增预处理:
for (ll i=1;i<=n;++i) for (ll j=1;(1<<j)<=dis[i];++j) F[j][i]=F[j-1][F[j-1][i]];
(跟 LCA 板子不一样的无非就是 LCA 时是在树上进行 dfs 时根据 dfs 序来确定当前要处理的节点,而此时因为更靠后得前缀的父亲一定在更靠前的前缀上,所以可以一个个的枚举 \(i\) 来处理第 \(i\) 个前缀)。
这里插一点关于树上倍增的一些注意事项:
-
预处理 \(F[i][j]\) 时,for 循环第一维和第二维的位置之间可以调换,但是一定注意:第一维枚举当前要处理的点时,一定要满足先处理“辈分”更大的节点,否则只能把要处理的点当第二维处理。
-
把 \(F[i][j]\) 的第一维 \(i\) 作为“辈分维”,第二位作为“节点维”是相比反过来极大的优化。
然后跳的时候像这样:
ll x=i,now=0;
for (ll j=19;j>=0;--j) if (F[j][x]) now+=1<<j,x=F[j][x];
就跟倍增求LCA时流程一样,一级一级往上跳就完了。
这个就是刚刚讲的知识的一个模板。
例题:【模板】失配树
这个就是在树上求个LCA。
但是有个坑点就是调用 LCA 函数时要调 \(LCA(next[x],next[y])\)。因为每个前缀的最长前缀都不能是自己本身。
exKMP
——求一个字符串每个后缀与他的最长公共前缀长度。
模板:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x7fffffff
const ll maxn=2000010;
ll z[maxn];
char a[maxn],b[maxn];
ll lena,lenb;
inline void exkmp() {
z[1]=lena+lenb;
ll l=0,r=0;
for (ll i=2;i<=lena+lenb;++i) {
if (i<=r) z[i]=min(z[i-l+1],r-i+1);
while (i+z[i]<=lena+lenb&&b[i+z[i]]==b[1+z[i]]) z[i]++;
if (i+z[i]-1>r) l=i,r=i+z[i]-1;
}
}
inline ll in() {
char a=getchar();
ll t=0,f=1;
while(a<'0'||a>'9') {if (a=='-') f=-1;a=getchar();}
while(a>='0'&&a<='9') {t=(t<<1)+(t<<3)+a-'0';a=getchar();}
return t*f;
}
signed main() {
scanf("%s%s",a+1,b+1);
lena=strlen(a+1),lenb=strlen(b+1);
for (ll i=1;i<=lena;++i) b[i+lenb]=a[i];
exkmp();
ll ansa=0,ansb=0;
ansa^=1ll*(lenb+1)*1;
for (ll i=2;i<=lenb;++i) ansa^=1ll*(min(z[i],lenb-i+1)+1)*i;
for (ll i=1;i<=lena;++i) ansb^=1ll*(min(z[i+lenb],lenb)+1)*i;
printf("%lld\n%lld",ansa,ansb);
return 0;
}
重点:理解zbox

浙公网安备 33010602011771号