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;
}

核心思想:

首先预处理模式串的最长前后缀,之后只移动模式串的指针根据我们预处理出的前后缀(加速)进行匹配。

预处理前后缀的作用是加速,也是整个算法的核心。

算法讲解视频||kmp流程演示


系列技巧(做题笔记):

  • 字符串最小循环节(周期) \(=\) 字符串长度 \(-\) 最长公共前后缀

模板题:[BOI2009]Radio Transmission 无线传输

证明

  • 在 kmp 过程中和栈结合

[USACO15FEB] Censoring S

要记录消除所有模式串之后的文本串,我们可以用一个栈记录当前我们遍历到的所有字符,若匹配成功,将栈顶弹出 \(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时流程一样,一级一级往上跳就完了。

例题:P2375 [NOI2014] 动物园

这个就是刚刚讲的知识的一个模板。

例题:【模板】失配树

这个就是在树上求个LCA。

但是有个坑点就是调用 LCA 函数时要调 \(LCA(next[x],next[y])\)。因为每个前缀的最长前缀都不能是自己本身。


exKMP

——求一个字符串每个后缀与他的最长公共前缀长度。

模板:

【模板】扩展 KMP(Z 函数)

#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

视频详解

posted @ 2023-06-20 16:48  Pwtking  阅读(46)  评论(0)    收藏  举报