KMP入门和简单运用

前言

这是一个咕了将近半年的文章,并不需要什么前置知识,只需要看的懂一些数学表达式就好了。

符号约定

  • ∣ S ∣ |S| S表示字符串 S S S的长度。
  • S [ l . . . r ] S[l...r] S[l...r]表示由第 l l l个到 r r r个字符组成的 S S S的子串,位置由 1 1 1开始。
  • S u f ( S ) \mathrm{Suf}(S) Suf(S)表示字符串 S S S的所有后缀构成的集合, S u f ′ ( S ) \mathrm{Suf'}(S) Suf(S)表示除去自身的所有后缀构成的集合, S u f ( S , l ) \mathrm{Suf}(S,l) Suf(S,l)表示 S S S长度为 l l l的后缀。
  • P r e ( S ) , P r e ′ ( S ) , P r e ( S , l ) \mathrm{Pre}(S),\mathrm{Pre'}(S), \mathrm{Pre}(S, l) Pre(S),Pre(S),Pre(S,l)同理。

讲解

B o r d e r \mathrm{Border} Border

B o r d e r ( S ) \mathrm{Border}(S) Border(S)表示字符串 S S S相同前缀后缀构成的集合(这里用他们的长度记录)。用形式化的表示就是:
B o r d e r ( S ) = { l ∣ 0 ≤ l < ∣ S ∣ , P r e ( S , l ) = S u f ( S , l ) } \mathrm{Border}(S)=\{l|0 \leq l < |S|, \mathrm{Pre}(S, l)=\mathrm{Suf}(S, l)\} Border(S)={l0l<S,Pre(S,l)=Suf(S,l)}
对于 l ∈ B o r d e r ( S ) l\in \mathrm{Border}(S) lBorder(S),我们称 P r e ( S , l ) \mathrm{Pre}(S,l) Pre(S,l) S u f ( S , l ) \mathrm{Suf}(S,l) Suf(S,l) S S S B o r d e r \mathrm{Border} Border,可以发现,空串是所有串的 B o r d e r \mathrm{Border} Border

可以发现一个简单的性质:
∀    l ∈ B o r d e r ( S ) , B o r d e r ( P r e ( S , l ) ) = B o r d e r ( S u f ( S , l ) ) ⊂ B o r d e r ( S ) \forall~~l \in \mathrm{Border}(S), \mathrm{Border}(\mathrm{Pre}(S,l))=\mathrm{Border}(\mathrm{Suf}(S,l))\sub \mathrm{Border}(S)   lBorder(S),Border(Pre(S,l))=Border(Suf(S,l))Border(S)
这个比较显然,用通俗语言概括就是我的 B o r d e r \mathrm{Border} Border B o r d e r \mathrm{Border} Border是我的 B o r d e r \mathrm{Border} Border

f a i l ( S ) \mathrm{fail}(S) fail(S)表示最长 B o r d e r \mathrm{Border} Border,形式化就是(很蠢):
f a i l ( S ) = P r e ( S , max ⁡ l ∈ B o r d e r ( S ) { l } ) \mathrm{fail}(S)=\mathrm{Pre} \left( S, \max_{l\in \mathrm{Border}(S)} \{l\} \right) fail(S)=Pre(S,lBorder(S)max{l})
接着找一下规律:
∣ f a i l ( S ) ∣ ∈ B o r d e r ( S ) ∣ f a i l ( f a i l ( S ) ) ∣ ∈ B o r d e r ( S ) ∣ f a i l ( f a i l ( f a i l ( S ) ) ) ∣ ∈ B o r d e r ( S ) . . . \begin{aligned} \left| \mathrm{fail}(S) \right| & \in \mathrm{Border}(S)\\ \left| \mathrm{fail}(\mathrm{fail}(S)) \right| & \in \mathrm{Border}(S)\\ \left| \mathrm{fail}(\mathrm{fail}(\mathrm{fail}(S))) \right| & \in \mathrm{Border}(S)\\ ... \end{aligned} fail(S)fail(fail(S))fail(fail(fail(S)))...Border(S)Border(S)Border(S)
那么可以发现,存在
∣ f a i l i ( S ) ∣ ∈ B o r d e r ( S ) \left|\mathrm{fail}^i(S)\right|\in\mathrm{Border}(S) faili(S)Border(S)
那么我们能否通过 f a i l ( S ) \mathrm{fail}(S) fail(S)找到所有的 B o r d e r \mathrm{Border} Border呢,答案是可行的,至于证明,用反证法搞一搞就好了,没什么难的。

求解 f a i l \mathrm{fail} fail

容易想到 O ( n 2 ) O(n^2) O(n2)的做法,由于太简单就不贴出来了,因为我们需要 O ( n ) O(n) O(n)的。

首先可以发现,假设我们求出了字符串 S S S中长度为 1... n 1...n 1...n的所有前缀的 f a i l ( P r e ( S , i ) ) \mathrm{fail}(\mathrm{Pre}(S,i)) fail(Pre(S,i)),那么很明显, f a i l ( P r e ( S , i + 1 ) ) \mathrm{fail}(\mathrm{Pre}(S,i+1)) fail(Pre(S,i+1))最多只会增加 1 1 1,否则就是不断缩小了,而具体怎么缩小,本来需要查看 B o r d e r ( S i ) \mathrm{Border}(S_i) Border(Si)中所有的值,看看是否有能够拓展的,但是按照我们先前得到的性质,只需要往回找到一个 f a i l ( P r e ( S , j ) ) \mathrm{fail}(\mathrm{Pre}(S,j)) fail(Pre(S,j))(设为 t t t),满足 S t + 1 = S i + 1 S_{t+1}=S_{i+1} St+1=Si+1或者 j = 0 j=0 j=0即可。

容易想到代码实现(这里的 S S S变成了 T T T):

n = strlen(T + 1), fail[1] = 0;
for (int i = 2, j = 0; i <= n; i++) {
    while(j && T[i] != T[j + 1]) j = fail[j];
    if (T[i] == T[j + 1]) j++;
    fail[i] = j;
}

求匹配

现在我们对模式串 T T T求出了 f a i l \mathrm{fail} fail,现在要用 T T T来匹配 S S S,那么我们可以使用类似的代码进行匹配:

m = strlen(S + 1);
for (int i = 1, j = 0; i <= m; i++) {
        while(j && (S[i] != T[j + 1] || j == n)) j = fail[j];
        if (S[i] == T[j + 1]) j++;
        f[i] = j; // 如果f[i]==n 则表示S[i-n+1...i]=T
    }

f a i l   T r e e \mathrm{fail~Tree} fail Tree

对于一个字符串 S S S的所有前缀,我们都计算出了他们的 f a i l \mathrm{fail} fail。现在我们转换一下定义 f a i l ( i ) \mathrm{fail}(i) fail(i)表示 P r e ( S , i ) \mathrm{Pre}(S,i) Pre(S,i)的最长 B o r d e r \mathrm{Border} Border容易想到构建一颗 f a i l \mathrm{fail} fail树,其中儿子指向父亲的边可以表示为 i ⟶ f a i l ( i ) i\longrightarrow \mathrm{fail}(i) ifail(i),显然根据这个定义,一个点到跟的路径上的所有点(不包括自己)都是这个点所代表的前缀的 B o r d e r \mathrm{Border} Border,且他们的长度随着深度的减小而减小。那么可以发现,在这颗树上,两个点的LCA就是这两个点所代表的前缀的最长公共 B o r d e r \mathrm{Border} Border

例题

Power Strings

求一个字符串由多少个重复的子串连接而成,也就是求循环节的循环次数。

例如 ababab 由三个 ab 连接而成,abcd 由一个 abcd 连接而成。

容易想到使用 f a i l \mathrm{fail} fail的奇怪性质,假设 f a i l ( n ) ≤ ⌊ n 2 ⌋ \mathrm{fail}(n)\leq \lfloor\frac{n}{2}\rfloor fail(n)2n,那么我们画一个图:
在这里插入图片描述

因为 B o r d e r \mathrm{Border} Border的性质,可以得到:
在这里插入图片描述

就这样不停地用 P r e ( S , n − f a i l ( n ) ) \mathrm{Pre}(S,n-\mathrm{fail}(n)) Pre(S,nfail(n))填充 S S S,假如剩下了一节长度小于 n − f a i l ( n ) n-\mathrm{fail}(n) nfail(n)的,那么就一定无解。

若恰好可以填满,那么我们可以证明, P r e ( S , n − f a i l ( n ) ) \mathrm{Pre}(S,n-\mathrm{fail}(n)) Pre(S,nfail(n))一定是 S S S最小的循环节。

那么差不多可以得到问题的答案啦:
a n s = { n n − f a i l ( n ) ( n − f a i l ( n ) ) ∣ n 1 o t h e r w i s e ans=\begin{cases} \cfrac{n}{n-\mathrm{fail}(n)} & (n-\mathrm{fail}(n))|n\\ 1 & otherwise \end{cases} ans=nfail(n)n1(nfail(n))notherwise

[NOI2014]动物园

给定 N N N个字符串,对于每一个字符串,算出它每一个前缀 P r e ( S , i ) \mathrm{Pre}(S,i) Pre(S,i)不超过该前缀一半的 B o r d e r \mathrm{Border} Border的数量,称为 n u m ( i ) \mathrm{num}(i) num(i)。你需要将每一个前缀的 n u m ( i ) + 1 \mathrm{num}(i)+1 num(i)+1乘起来,作为字符串 S S S的答案。答案对 1 0 9 + 7 10^9+7 109+7取模。

我们只需要将 n u m ( i ) \mathrm{num}(i) num(i)都求出来就好了。

很显然, n u m ( i ) \mathrm{num}(i) num(i)一定是 f a i l ( i ) \mathrm{fail}(i) fail(i) f a i l   T r e e \mathrm{fail~Tree} fail Tree上的一个祖先,那么最暴力的方法就是对于每一个前缀,暴力地找符合条件的点。

但是数组开不下,而且很容易 T L E TLE TLE,那么就需要 O ( n ) O(n) O(n)的方法了。

首先在求 f a i l \mathrm{fail} fail的时候,顺道求出 i i i f a i l   T r e e \mathrm{fail~Tree} fail Tree上到根路径上的节点个数(包括 i i i自身),这里设为 c n t i cnt_i cnti。那么显然, n u m ( i ) \mathrm{num}(i) num(i)就是到根路径上的某个 c n t j cnt_j cntj。接着我们可以发现, n u m ( i ) \mathrm{num}(i) num(i)每右移一位的时候,至多增加 1 1 1,也就是说,我们可以使用类似匹配文本串的方式,加一个小判断,进而求出 j j j

for (int i = 2, j = 0; i <= n; i++) {
    while (j && str[i] != str[j+1]) j = fail[j];
    if (str[i] == str[j+1]) j++;
    if ((j<<1) > i) j = fail[j]; // j至多增加1,也就是说至多跳到i/2+1
    ans = ans * (cnt[j] + 1) % MOD;         
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值