zoukankan      html  css  js  c++  java
  • 浅谈字符串匹配的KMP算法

    今天跟qs聊了会,她放出了我的小时的照片,毕竟黑历史谁都有

    然后,Singercoder 极限卡篮,还有就是,我又掉 rating 了,我也想去 NOI

    PS: Singercoder 掉青 2020/3/7

    update: 2020/3/23 相应 Singercoder 所做的笔记

    个人认为 Singercoder 的笔记写的是真的清晰 ,但无奈码风过于毒瘤了

    KMP

    前言

    KMP 算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。

    说句闲话

    先说一下字符串匹配的定义,就是一个给你一个主串 S 一个模式串 T 然后求 T 在 S 里出现的每个位置

    没有学KMP之前,我会两种字符串匹配算法,一个是朴素匹配,一个是有限自动机(不算是会,就是了解一下)但是这两个算法复杂度无疑可以卡成 (O(nm)), 是无法接受的,然而这闲的没事干的三人把字符串匹配做成了线性的 %%%。

    但是似乎在 oi 里的用处不是很大吧,我从来也没在题里用过他,除了板子题

    正文

    来介绍一种 Knuth-Morris-Pratt 算法 这个算法可以做到在 (O(n + m)) 时间内完成模式串和主串的匹配,利用了前缀的一些性质,用到了辅助函数 (pi)

    看算法的请往跳过,这里说的是原理

    关于模式的前缀函数

    这个算法的核心思想就在于此了,一个 (pi) 函数。它包含了模式与其自身的偏移的信息。这些信息在朴素匹配中没有利用到所以慢。

    考察一下朴素字符串匹配的操作过程,下图正是朴素里一个匹配中的情景

    我们可以看到,(q = 5) 个字符已经匹配成功了,那么我们知道的这 (q) 个字符可以为我们知道一些不必要的偏移了。在这个实例中,显然偏移为 s + 1 是无用的,因为第一个字符 (a) 将于文本匹配,但是该模式串的第二个字符 (b) 并不能于匹配。如下图所示,偏移 s' = s + 2 使模式串的前三个字符与后三个字符匹配

    前方高能!

    我们知道下面问题的 answer 是很有用的:

    假设模式串 (T[1...q]) 和主串 (S[s + 1.. S+q]) 匹配,(s') 是最小的偏移量, (s' > s) , 那么对于某些 (k < q) ,满足

    [T[1..k] = S[s'+1..s'+k] ]

    的最小偏移 (s' > s) 其中 (s' + k = s + q) 是什么?

    话句话说,已知 (T_qsqsupset S_{s+q}) ((asqsupset b) 表示 a 是 b 的后缀) 我们想要找到 (T_q) 的最长真前缀 k,也是 (S_{s+q}) 的后缀(找到 (k) 等价于写出了 (s') ) 显然的有 $s'= s + q - k $ .于是我们可以预先处理出这些 (k)(pi) 数组存起来.

    并且我们发现,求解这些信息就是一个 (T) 与其自身的匹配过程,下面来模拟一下过程以形象的说明.已知一个模式串 (T[1..m]) 前缀函数 (pi : {1...m} ightarrow{0...m - 1}) 满足

    [pi[i] = max(k:k<i且 T_k sqsupset Ti) ]

    下图给出了一个完整的 (pi) 函数

    我们求解这个数组无疑就是自身与自身的匹配过程

    代码实现

    给出一种很巧妙的方法,我们不妨先设 pi[0] = -1 这可以减少码量

    我们呢不妨假设 (pi) 已求出,现在要主串S与模式串T匹配

    具体的时候,因为实现的问题,我们无需记录偏移量 (s) ,而是维护两个指针 (i,j).

    for(i = 1; i <= lens; i++){
    	while(j > -1 && T[j+1] != S[i]) j = pi[j];
    	if(++j == lent) printf("%d
    ", i - lenb + 1);
    }
    

    可以理解为如果下一个字符不匹配那么就要做偏移了.直到一样或者无法偏移了

    $ pi$ 就是 (T) 与其本身的每个后缀的最长公共前缀.我们求解 $ pi$ 的过程就是 (T) 与其本身的的匹配(但是不要从第一个开始)

    特别地我们有一条引理 (pi[i] le pi[i - 1] + 1)

    所以程序写的十分巧妙.

    pi[0] = -1;
    for(int i = 1; i <= lenb; i++){
    	int j = pi[i - 1];
    	while(j > -1 && T[i] != T[j+1]) j = pi[j];
    	pi[i]=j+1;
    }
    

    exKMP

    PS:会补充的

    exKMP 解决的是这么一个问题,给你主串 S 和 字串 T,问题对于 S 的每个后缀 ,与 T 的最长公共前缀的长度

    我们先定义数组:

    [z[i] : 表示 T 的每个后缀与 T 的最长公共前缀的长度\ p[i] : 表示 S 的每个后缀与 T 的最长公共前缀的长度\ ]

    在这里,我们不妨设 z 已求出,我们的任务是求解 p

    在下面的表述方法中,对于一个字符串 S,记 (S_i)S 的第 i 个字符,(S_{l sim r})S 从第 l 个字符起到第 r 个字符结束形成的子串

    考虑从前向后地计算 p ,假设我们要计算 (p_i),那么由 (forall j in [1,i),p_i) 已算出

    l 是目前已经计算过的位置中,向由扩展的最长前缀的首字母的位置,即 l 满足 (p_l +l) 最大,记 r 为这个值,显然的有 (r ge i-1)

    我们在此时就开始分类 :

    part 1

    我们讨论 (r = i - 1) 的情况,我没在之前没有能利用的信息,于是,直接暴力匹配就好。

    part 2

    我们讨论 (r > i - 1) 的情况。这可就棘手了。当时没听懂 ,首先记 (j = i - l +1)

    故而我们在做 l 的时候,就知道 (S_{lsim i}= T_{1 sim j})

    于是,考虑我们要重复匹配的区间,其长度自然是 (r-i+1),比较它与 (z_j) 的关系,分两种情况讨论

    part 2.1

    (z_j<r-i+1) ,则有 (T_{1sim z_j}=T_{ j sim j+z_j-1}),又因 (S_{isim r}=T_{jsim j+(r-i+1)}),于是我们有 (S_{i_i+z_j-1}=T_{jsim j+z_j-1}=T_{1sim z_j}),而显然的有 (S_{i+z_j}=T_{j+z_j} ot= T_{z_j+1})

    于是有 (p_i=z_j=z_{i-l+1})

    part2.2

    照着前一个 能证明 (p_i ge r-i+1),后面不造故而暴力匹配

    然后做 l,r 的的更新即可

    z 数组的求解无非就是 T 本身的匹配,只要初始化 (z_1) 即可

    献上代码

    #define next z
    #define extand p
    
    void Get_Z(){
    	next[1] = lenb;
    	for(int i = 2, l = 0, r = 0; i <= lenb; ++i) {
    		if(i <= r) next[i] = min(next[i - l + 1], r - i + 1);//先看看
    		while(i + next[i] <= lenb && b[i + next[i]] == b[next[i] + 1]) ++next[i];//然后暴力
    		if(i + next[i] -1 > r) l = i, r = i + next[i] - 1;//再更新
    	}
    }
    
    void Get_P(){
    	for(int i = 1, l = 0, r = 0;i <= lena; ++i) {
    		if(i <= r) extand[i] = min(next[i - l + 1], r - i + 1);
    		while(i + extand[i] <= lena && a[i + extand[i]] == b[extand[i] + 1]) ++extand[i];
    		if(i + extand[i] - 1 > r) l = i, r = i + extand[i] - 1; 
    	}
    }
     
    
  • 相关阅读:
    PAT 甲级 1126 Eulerian Path (25 分)
    PAT 甲级 1126 Eulerian Path (25 分)
    PAT 甲级 1125 Chain the Ropes (25 分)
    PAT 甲级 1125 Chain the Ropes (25 分)
    PAT 甲级 1124 Raffle for Weibo Followers (20 分)
    PAT 甲级 1124 Raffle for Weibo Followers (20 分)
    PAT 甲级 1131 Subway Map (30 分)
    PAT 甲级 1131 Subway Map (30 分)
    AcWing 906. 区间分组 区间贪心
    AcWing 907. 区间覆盖 区间贪心
  • 原文地址:https://www.cnblogs.com/zhltao/p/12556968.html
Copyright © 2011-2022 走看看