KMP笔记(旧)

符号

  • 子串:字符串中连续的一段。
  • p r e ( s , x ) pre(s, x) pre(s,x): s [ 1... x ] s[1 ... x] s[1...x] 组成的子串。
  • s u f ( s , x ) suf(s, x) suf(s,x): s [ ∣ s ∣ − x . . . ∣ s ∣ ] s[|s| - x ... |s|] s[sx...∣s] 组成的子串。
  • 周期:若 p p p 为字符串 s s s 的周期,则 p p p 满足
    • 0 < p < ∣ s ∣ 0 < p < |s| 0<p<s.
    • s [ i ] = s [ i + p ] , i ∈ { 1 , 2 , 3... , ∣ s ∣ − p } s[i] = s[i + p], i \in \left\{1, 2, 3..., |s| - p \right\} s[i]=s[i+p],i{1,2,3...,sp}.
  • b o r d e r border border: 若 p r e ( s , r ) pre(s, r) pre(s,r) 称为字符串 s s s b o r d e r border border,则 r r r 满足
    • 0 < r < ∣ s ∣ 0 < r < |s| 0<r<s.
    • p r e ( s , r ) = s u f ( s , r ) pre(s, r) = suf(s, r) pre(s,r)=suf(s,r)

p e r i o d period period b o r d e r border border

  • 3 3 3 6 6 6 都是 a b c a b c a b abcabcab abcabcab 的周期。
    • 注意,一个周期不需要恰好能整除串长。
  • a b c a b abcab abcab a b ab ab 都是 a b c a b c a b abcabcab abcabcab b o r d e r border border
  • p r e ( s , k ) pre(s, k) pre(s,k) s s s b o r d e r border border ⇔ \Leftrightarrow ∣ s ∣ − k |s| - k sk s s s 的周期。
    • 画图,自证不难.

b o r d e r border border 的性质

  • (传递性1)若 S S S T T T b o r d e r border border, T T T R R R b o r d e r border border, 则 S S S R R R b o r d e r border border.
  • (传递性2)若 S S S R R R b o r d e r border border, T T T R R R b o r d e r border border, 且 ∣ S ∣ < ∣ T ∣ |S| < |T| S<T,则 S S S T T T b o r d e r border border.
  • 以上两点画图显然.
  • (封闭性)记 m b ( S ) mb(S) mb(S) S S S 的最长 b o r d e r border border,则 m b ( m b ( S ) ) , m b ( m b ( m b ( S ) ) ) . . . mb(mb(S)), mb(mb(mb(S)))... mb(mb(S)),mb(mb(mb(S)))... S S S 的所有 b o r d e r border border.
    • 由 传递性1、2 得.

KMP模式匹配

  • 在主串 S S S 中查找 模式串 T T T 第一次出现的位置。
    • 暴力匹配 O ( ∣ S ∣ ⋅ ∣ T ∣ ) O(|S|\cdot|T|) O(ST),
  • 考虑 S S S i i i 结尾,长度为 j j j 的子串 S [ i − j . . . i ] S[i - j...i] S[ij...i],发现,字符串的任意子串总是该字符串的一个前缀的后缀,即 S [ i − j . . . i ] = s u f ( p r e ( S , i ) , j ) S[i - j...i] = suf(pre(S, i), j) S[ij...i]=suf(pre(S,i),j).
  • 观察暴力匹配的过程,我们用两个指针 i , j i, j i,j,分别对应在 S , T S, T S,T 中扫描。
    • 每次扫描开始时,都应有 S [ i . . . i + j ] = s u f ( p r e ( S , i + j ) , j ) = p r e ( T , j ) S[i...i + j] = suf(pre(S, i + j), j) = pre(T, j) S[i...i+j]=suf(pre(S,i+j),j)=pre(T,j)
    • S [ i + j + 1 ] = T [ j + 1 ] S[i + j + 1] = T[j + 1] S[i+j+1]=T[j+1],则将 j j j 往后移一位.
    • S [ i + j + 1 ] ≠ T [ j + 1 ] S[i + j + 1] \neq T[j + 1] S[i+j+1]=T[j+1],则将 i i i 往后移一位, j j j 从头开始扫描.
    • 复杂度为 O ( ∣ S ∣ ⋅ ∣ T ∣ ) O(|S|\cdot|T|) O(ST).
  • 暴力慢在哪呢?慢在 i , j i, j i,j 往后移的步骤上。如果我们能够跳过一些肯定匹配不上的位置,或许能更快!
  • 这时,K、M、P 三人提出了一个KMP策略 :
    • 我们用两个指针 i , j i, j i,j 分别表示, S [ i − j + 1... i ] S[i - j + 1...i] S[ij+1...i] T [ 1... j ] T[1...j] T[1...j] 完全相等,即 S S S 匹配到 i i i, T T T 匹配到 j j j.
    • 首先介绍 KMP性质: 每次扫描开始时,都应有 s u f ( p r e ( S , i ) , j ) = p r e ( T , j ) suf(pre(S, i), j) = pre(T, j) suf(pre(S,i),j)=pre(T,j), 且 j j j 越大越好.
    • S [ i + 1 ] = T [ j + 1 ] S[i + 1] = T[j + 1] S[i+1]=T[j+1],则将 i i i, j j j 各往后移一位,满足KMP性质.
    • S [ i + 1 ] ≠ T [ j + 1 ] S[i + 1] \neq T[j + 1] S[i+1]=T[j+1],也就是失配了,我们该怎么调整指针来避免每次都重新开始匹配呢?
      1. 由 KMP性质,我们知道此时的 i , j i, j i,j 肯定满足 s u f ( p r e ( S , i ) , j ) = p r e ( T , j ) suf(pre(S, i), j) = pre(T, j) suf(pre(S,i),j)=pre(T,j),因而它们的 b o r d e r border border 也相同.
      2. 于是,我们每次将 j j j 跳跃到 p r e ( T , j ) pre(T, j) pre(T,j) 当前最长的 b o r d e r border border 上,不难发现若能匹配下去,这个最长的 b o r d e r border border 一定能匹配得最长,且满足 KMP性质.
      3. 如果还是不能匹配,接着跳 b o r d e r border border,直到 j = − 1 j = -1 j=1.
      4. 结合封闭性,我们只需求出 T T T 每个前缀的最长 b o r d e r border border 就可以快速进行上述步骤.
    • p r e ( T , j ) pre(T, j) pre(T,j) 的最长 b o r d e r border border n e x t [ j ] next[j] next[j],以下是利用 n e x t next next 数组匹配的代码。
int kmp(string s, string t) {
    int n = s.size(), m = t.size();
    int j = -1;
    for (int i = 0; i < n; i++) {
        while (j >= 0 && s[i] != t[j + 1]) j = next[j];
        if (s[i] == t[j + 1]) j++;
        if (j + 1 == m) { //匹配成功
            return i - m + 1;
        }
    }
    return -1; //匹配失败
}
  • j j j 后移的次数最多为 ∣ T ∣ |T| T j j j 回退的次数总是少于后移的次数,因而匹配复杂度摊还 O ( ∣ S ∣ ) O(|S|) O(S).
  • 把子串看作一个前缀的真后缀,不难发现求解 n e x t next next 的方法与匹配类似,以下是求解 n e x t next next 的代码.
void init(string t) {
    int m = t.size();
    next[0] = -1;
    int j = -1;
    for (int i = 1; i < m; i++) {
        while (j >= 0 && t[i] != t[j + 1]) j = next[j];
        if (t[i] == t[j + 1]) j++;
        next[i] = j;
    }
}
  • 预处理 n e x t next next 的复杂度摊还 O ( ∣ T ∣ ) O(|T|) O(T),因此总复杂度 O ( ∣ S ∣ + ∣ T ∣ ) O(|S|+|T|) O(S+T).

  • 注意:我习惯以 0 0 0 为字符串下标的起点,因此 n e x t next next 的初值赋为 − 1 -1 1,在调用 b o r d e r border border 长度时应加上 1 1 1

  • 虽然蛮讨厌的,但在构造失配树的时候下标最好还是从 1 1 1 开始。

失配树

  • 定义:将 n e x t [ i ] next[i] next[i] 看作 i i i 的父节点,那么通过 n e x t next next 数组可以把 0 ∼ N 0\sim N 0N 点连成一棵
  • 性质:
    • i i i 的所有祖先都是 p r e ( s , i ) pre(s,i) pre(s,i) b o r d e r border border.
    • 没有祖先关系的两个点 i , j i, j i,j 没有 b o r d e r border border 关系.
    • 任意两点的 L C A LCA LCA 为它们的最长公共 b o r d e r border border.
  • 结合失配树,计算 n e x t [ i ] next[i] next[i] 的过程可以看作:从 j = f a [ i − 1 ] j = fa[i - 1] j=fa[i1] 开始不断往上走,找第一个满足 s [ j + 1 ] = s [ i ] s[j + 1] = s[i] s[j+1]=s[i] 的点,把 i i i 的父亲设为 j + 1 j + 1 j+1.
    • 为什么是这样呢?
    • 考虑失配树的定义, j = f a [ i − 1 ] ⇔ j = fa[i - 1] \Leftrightarrow j=fa[i1] s [ 0... j ] s[0...j] s[0...j] p r e ( s , i − 1 ) pre(s, i - 1) pre(s,i1) 的最长 b o r d e r border border.
    • 又因为 f a [ j ] fa[j] fa[j] p r e ( s , j ) pre(s,j) pre(s,j) 的最长 b o r d e r border border,所以向上走寻找匹配的过程就是利用 KMP策略 求 n e x t [ i ] next[i] next[i] 的过程.
  • 代码就不放了。

例题

  • 【例1】P4391 [BOI2009]Radio Transmission 无线传输
    • 题意简述:求一个串的最小周期。
    • 由上文提到的 “ p e r i o d period period b o r d e r border border 的关系”,我们知道 最小周期 为 串长减去最长的 b o r d e r border border.
    • 回想 n e x t next next 数组的定义,此题得解.
  • 【例2】P3435 [POI2006] OKR-Periods of Words
    • 题意简述:求一个串所有前缀的最大周期。
    • 最大周期 即为 串长减去最短的 b o r d e r border border,求出最短的 b o r d e r border border 就万事大吉了.
    • 回想 失配树 的性质,最短的 b o r d e r border border 不就是该点到根的路径上离根最近的点吗?想到这里就有很多搞法了.
    • 可以建出失配树,然后枚举失配树的每一个 儿子,从每一个儿子出发往下跑一遍 d f s dfs dfs,由于每个节点只会被遍历一次,故复杂度是 O ( n ) O(n) O(n) 的.
    • 也可以利用并查集思想,把每一个与根节点直接相连的儿子作为代表元,路径压缩后即可快速查询.
  • 【例3】POJ2185 Milking Grid
    • 题意简述:求一个二维字符矩阵的最小周期。
    • 行列分别跑一次 KMP,在跑列的 KMP 时,把每行看作一个整体,每次检验时暴力检验所有行。行方向 KMP 同理。复杂度 O ( R × M ) O(R \times M) O(R×M)
  • 【例4】P2375 [NOI2014] 动物园
    • 题意简述:求出一个串每一个前缀的 b o r d e r border border 中,长度不超过该前缀长度一半的 b o r d e r border border 的数量。
    • 上失配树,每个节点 i i i 都倍增跳到第一个满足 j ≤ i / 2 j \leq i/2 ji/2 的点 j j j 上,然后统计。时间 O ( T n l o g n ) O(Tnlogn) O(Tnlogn),常数小的做法可以卡过.
  • 【例5】P3426 [POI2005]SZA-Template
    • 题意简述:求字符串最小的一个前缀,使得它拼接(可以重叠)若干次后恰好能还原字符串。
    • p r e ( s , i ) pre(s, i) pre(s,i) "恰好"能还原原串,说明该长度为 i i i 的前缀与长度为 i i i 的后缀是相同的,也就是说答案是一个 b o r d e r border border.
    • 那只要是 b o r d e r border border,就一定能还原吗?不是,该 b o r d e r border border 在原串中匹配的位置之间间隔不能超过 b o r d e r border border 的长度.
    • 于是就有了很多想法。
    • 一种是上失配树,那么符合条件的 b o r d e r border border 一定是 ∣ s ∣ |s| s 的祖先节点.
    • 枚举 ∣ s ∣ |s| s 的所有祖先,设当前在点 i i i,那么 i i i 的子树中相邻节点的距离不能超过 i i i,否则就会空出一段.
    • 另一种想法是直接 d p dp dp,设 f i f_i fi 是覆盖 p r e ( s , i ) pre(s, i) pre(s,i) 的最小 b o r d e r border border,则 f i f_i fi 只有 2 2 2 种取值: i i i f n e x t i f_{next_i} fnexti.
    • 为什么是 f n e x t i f_{next_i} fnexti 呢?由上面的分析我们知道 f i ≤ n e x t i f_i \leq next_i finexti,凡是不能覆盖 n e x t i next_i nexti 的,一定覆盖不了 p r e ( s , i ) pre(s, i) pre(s,i),因此 f i f_i fi 只能由 f n e x t i f_{next_i} fnexti 转移过来.
    • 那么什么时候 f i f_i fi 能从 f n e x t i f_{next_i} fnexti 转移呢?因为 b o r d e r border border 在原串中匹配的位置之间间隔不能超过 b o r d e r border border 的长度,因此,只有存在一个 j j j,满足 i − n e x t i ≤ j i - next_i \leq j inextij f j f_j fj 是由 f n e x t i f_{next_i} fnexti 转移来的时候, f i f_i fi 才能等于 f n e x t i f_{next_i} fnexti.
    • 开个桶记录一下最后一个 f i = f n e x t i f_i = f_{next_i} fi=fnexti 的位置即可做到 O ( n ) O(n) O(n).
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值